resplite 1.2.6 → 1.2.8
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.
- package/README.md +168 -275
- package/package.json +1 -6
- package/scripts/create-interface-smoke.js +32 -0
- package/skills/README.md +22 -0
- package/skills/resplite-command-vertical-slice/SKILL.md +134 -0
- package/skills/resplite-ft-search-workbench/SKILL.md +138 -0
- package/skills/resplite-migration-cutover-assistant/SKILL.md +138 -0
- package/spec/00-INDEX.md +37 -0
- package/spec/01-overview-and-goals.md +125 -0
- package/spec/02-protocol-and-commands.md +174 -0
- package/spec/03-data-model-ttl-transactions.md +157 -0
- package/spec/04-cache-architecture.md +171 -0
- package/spec/05-scan-admin-implementation.md +379 -0
- package/spec/06-migration-strategy-core.md +79 -0
- package/spec/07-type-lists.md +202 -0
- package/spec/08-type-sorted-sets.md +220 -0
- package/spec/{SPEC_D.md → 09-search-ft-commands.md} +3 -1
- package/spec/{SPEC_E.md → 10-blocking-commands.md} +3 -1
- package/spec/{SPEC_F.md → 11-migration-dirty-registry.md} +61 -147
- package/src/commands/object.js +17 -0
- package/src/commands/registry.js +2 -0
- package/src/engine/engine.js +11 -0
- package/src/migration/apply-dirty.js +8 -1
- package/src/migration/index.js +5 -4
- package/src/migration/migrate-search.js +25 -6
- package/test/integration/object-idletime.test.js +51 -0
- package/test/unit/migrate-search.test.js +50 -2
- package/spec/SPEC_A.md +0 -1171
- package/spec/SPEC_B.md +0 -426
- package/src/cli/import-from-redis.js +0 -194
- package/src/cli/resplite-dirty-tracker.js +0 -92
- package/src/cli/resplite-import.js +0 -296
- package/test/contract/import-from-redis.test.js +0 -83
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
# RESPLite Specification v1 — SCAN, Admin Commands, Project Structure, Testing
|
|
2
|
+
|
|
3
|
+
## 17. SCAN Behavior
|
|
4
|
+
|
|
5
|
+
SCAN is part of the v1 scope.
|
|
6
|
+
|
|
7
|
+
### 17.1 Minimum supported form
|
|
8
|
+
|
|
9
|
+
Required in v1:
|
|
10
|
+
|
|
11
|
+
- `SCAN cursor`
|
|
12
|
+
|
|
13
|
+
### 17.2 Response shape
|
|
14
|
+
|
|
15
|
+
The response should follow Redis-like shape:
|
|
16
|
+
|
|
17
|
+
- next cursor
|
|
18
|
+
- array of keys
|
|
19
|
+
|
|
20
|
+
### 17.3 Implementation note
|
|
21
|
+
|
|
22
|
+
A simple initial strategy is acceptable, such as lexicographic traversal over `redis_keys.key`.
|
|
23
|
+
|
|
24
|
+
### 17.4 Future extensions
|
|
25
|
+
|
|
26
|
+
Possible later additions:
|
|
27
|
+
|
|
28
|
+
- `MATCH pattern`
|
|
29
|
+
- `COUNT n`
|
|
30
|
+
|
|
31
|
+
These are not required in the first implementation.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 18. Administrative Commands
|
|
36
|
+
|
|
37
|
+
### 18.1 SQLITE.INFO
|
|
38
|
+
|
|
39
|
+
This command should expose useful operational information such as:
|
|
40
|
+
|
|
41
|
+
- database path
|
|
42
|
+
- SQLite version
|
|
43
|
+
- counts by type
|
|
44
|
+
- total logical key count
|
|
45
|
+
- WAL mode status if available
|
|
46
|
+
|
|
47
|
+
### 18.2 CACHE.INFO
|
|
48
|
+
|
|
49
|
+
This command should expose cache information such as:
|
|
50
|
+
|
|
51
|
+
- cache enabled or disabled
|
|
52
|
+
- entry count
|
|
53
|
+
- estimated memory use
|
|
54
|
+
- hit count
|
|
55
|
+
- miss count
|
|
56
|
+
- hit ratio if available
|
|
57
|
+
|
|
58
|
+
These commands are intended for observability and debugging.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 19. Future Search Scope
|
|
63
|
+
|
|
64
|
+
Search is not part of the base v1 implementation, but the architecture must remain compatible with a near-term v1.1 search layer.
|
|
65
|
+
|
|
66
|
+
### 19.1 Search vision
|
|
67
|
+
|
|
68
|
+
The future search subsystem should provide RediSearch-inspired commands powered by SQLite FTS5 with BM25 ranking.
|
|
69
|
+
|
|
70
|
+
Candidate commands:
|
|
71
|
+
|
|
72
|
+
- `FT.CREATE`
|
|
73
|
+
- `FT.SEARCH`
|
|
74
|
+
- `FT.INFO`
|
|
75
|
+
- `FT.DROPINDEX`
|
|
76
|
+
|
|
77
|
+
### 19.2 Recommended document model
|
|
78
|
+
|
|
79
|
+
The preferred future approach is to index hashes as documents.
|
|
80
|
+
Example logical model:
|
|
81
|
+
|
|
82
|
+
- `article:1` stored as a hash
|
|
83
|
+
- an index with prefix `article:`
|
|
84
|
+
- fields such as `title`, `body`, and `category`
|
|
85
|
+
|
|
86
|
+
### 19.3 Backend strategy
|
|
87
|
+
|
|
88
|
+
- SQLite FTS5 virtual tables
|
|
89
|
+
- BM25 ranking
|
|
90
|
+
- auxiliary metadata tables for filtering and sorting
|
|
91
|
+
|
|
92
|
+
The project should not attempt to fully clone RediSearch grammar or behavior at the start.
|
|
93
|
+
A clean, reduced, Redis-like search surface is preferable.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 20. Project Structure
|
|
98
|
+
|
|
99
|
+
Recommended Node.js JavaScript ESM project layout:
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
resplite/
|
|
103
|
+
package.json
|
|
104
|
+
README.md
|
|
105
|
+
SPEC.md
|
|
106
|
+
src/
|
|
107
|
+
index.js
|
|
108
|
+
server/
|
|
109
|
+
tcp-server.js
|
|
110
|
+
connection.js
|
|
111
|
+
resp/
|
|
112
|
+
parser.js
|
|
113
|
+
encoder.js
|
|
114
|
+
types.js
|
|
115
|
+
commands/
|
|
116
|
+
registry.js
|
|
117
|
+
ping.js
|
|
118
|
+
echo.js
|
|
119
|
+
quit.js
|
|
120
|
+
get.js
|
|
121
|
+
set.js
|
|
122
|
+
del.js
|
|
123
|
+
exists.js
|
|
124
|
+
expire.js
|
|
125
|
+
pexpire.js
|
|
126
|
+
ttl.js
|
|
127
|
+
pttl.js
|
|
128
|
+
persist.js
|
|
129
|
+
incr.js
|
|
130
|
+
decr.js
|
|
131
|
+
incrby.js
|
|
132
|
+
decrby.js
|
|
133
|
+
type.js
|
|
134
|
+
scan.js
|
|
135
|
+
hset.js
|
|
136
|
+
hget.js
|
|
137
|
+
hmget.js
|
|
138
|
+
hgetall.js
|
|
139
|
+
hdel.js
|
|
140
|
+
hexists.js
|
|
141
|
+
hincrby.js
|
|
142
|
+
sadd.js
|
|
143
|
+
srem.js
|
|
144
|
+
smembers.js
|
|
145
|
+
sismember.js
|
|
146
|
+
scard.js
|
|
147
|
+
sqlite-info.js
|
|
148
|
+
cache-info.js
|
|
149
|
+
engine/
|
|
150
|
+
engine.js
|
|
151
|
+
errors.js
|
|
152
|
+
expiration.js
|
|
153
|
+
validate.js
|
|
154
|
+
storage/
|
|
155
|
+
sqlite/
|
|
156
|
+
db.js
|
|
157
|
+
schema.js
|
|
158
|
+
pragmas.js
|
|
159
|
+
keys.js
|
|
160
|
+
strings.js
|
|
161
|
+
hashes.js
|
|
162
|
+
sets.js
|
|
163
|
+
tx.js
|
|
164
|
+
cache/
|
|
165
|
+
lru.js
|
|
166
|
+
cache.js
|
|
167
|
+
util/
|
|
168
|
+
buffers.js
|
|
169
|
+
patterns.js
|
|
170
|
+
clock.js
|
|
171
|
+
logger.js
|
|
172
|
+
test/
|
|
173
|
+
helpers/
|
|
174
|
+
server.js
|
|
175
|
+
client.js
|
|
176
|
+
tmp.js
|
|
177
|
+
clock.js
|
|
178
|
+
fixtures.js
|
|
179
|
+
unit/
|
|
180
|
+
...
|
|
181
|
+
integration/
|
|
182
|
+
...
|
|
183
|
+
contract/
|
|
184
|
+
...
|
|
185
|
+
stress/
|
|
186
|
+
...
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## 21. Implementation Stack
|
|
192
|
+
|
|
193
|
+
Recommended initial stack:
|
|
194
|
+
|
|
195
|
+
- Node.js
|
|
196
|
+
- JavaScript ESM
|
|
197
|
+
- `better-sqlite3`
|
|
198
|
+
- built-in `node:test`
|
|
199
|
+
- `assert/strict`
|
|
200
|
+
- `redis` npm package for contract tests
|
|
201
|
+
|
|
202
|
+
The v1 codebase should prioritize correctness, testability, and operational simplicity over TypeScript or extensive framework usage.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## 22. Testing Strategy
|
|
207
|
+
|
|
208
|
+
Testing is a first-class requirement.
|
|
209
|
+
A command is not considered implemented until it has meaningful coverage.
|
|
210
|
+
|
|
211
|
+
### 22.1 Testing categories
|
|
212
|
+
|
|
213
|
+
The project must include the following categories of tests:
|
|
214
|
+
|
|
215
|
+
- unit tests
|
|
216
|
+
- integration tests
|
|
217
|
+
- contract tests
|
|
218
|
+
- persistence tests
|
|
219
|
+
- expiration tests
|
|
220
|
+
- consistency tests
|
|
221
|
+
- concurrency tests
|
|
222
|
+
- protocol framing tests
|
|
223
|
+
- binary-safety tests
|
|
224
|
+
|
|
225
|
+
### 22.2 Per-command testing rule
|
|
226
|
+
|
|
227
|
+
Each supported command should have tests for:
|
|
228
|
+
|
|
229
|
+
- normal behavior
|
|
230
|
+
- missing-key behavior where applicable
|
|
231
|
+
- wrong-type behavior where applicable
|
|
232
|
+
- TTL interaction where applicable
|
|
233
|
+
- persistence across restart where applicable
|
|
234
|
+
- binary-safe behavior where applicable
|
|
235
|
+
|
|
236
|
+
### 22.3 RESP protocol tests
|
|
237
|
+
|
|
238
|
+
The RESP layer must be tested for:
|
|
239
|
+
|
|
240
|
+
- valid parsing of RESP2 types
|
|
241
|
+
- partial frames split across TCP chunks
|
|
242
|
+
- multiple commands arriving in a single TCP chunk
|
|
243
|
+
- correct encoding of all response types
|
|
244
|
+
|
|
245
|
+
### 22.4 Contract tests with real clients
|
|
246
|
+
|
|
247
|
+
v1 contract tests should use:
|
|
248
|
+
|
|
249
|
+
- `redis-cli`
|
|
250
|
+
- the official `redis` npm client
|
|
251
|
+
|
|
252
|
+
### 22.5 Persistence tests
|
|
253
|
+
|
|
254
|
+
The project must verify:
|
|
255
|
+
|
|
256
|
+
- data survives server restart
|
|
257
|
+
- TTL metadata survives restart
|
|
258
|
+
- expired keys behave as missing after restart
|
|
259
|
+
- mixed key types remain valid after restart
|
|
260
|
+
|
|
261
|
+
### 22.6 Consistency tests
|
|
262
|
+
|
|
263
|
+
The project must verify internal invariants such as:
|
|
264
|
+
|
|
265
|
+
- rows in type tables must correspond to a valid row in `redis_keys`
|
|
266
|
+
- logical type must match actual storage table placement
|
|
267
|
+
- expired keys must not appear through command results
|
|
268
|
+
- empty hashes and sets are removed logically
|
|
269
|
+
|
|
270
|
+
### 22.7 Concurrency tests
|
|
271
|
+
|
|
272
|
+
The project must verify behavior under multiple clients, including:
|
|
273
|
+
|
|
274
|
+
- many concurrent INCR operations on the same key
|
|
275
|
+
- reads and writes overlapping across connections
|
|
276
|
+
- expiration sweeps occurring during active traffic
|
|
277
|
+
- restart safety under recent write activity
|
|
278
|
+
|
|
279
|
+
### 22.8 Performance sanity tests
|
|
280
|
+
|
|
281
|
+
The project does not need benchmark-grade tests in v1, but it should include sanity checks such as:
|
|
282
|
+
|
|
283
|
+
- repeated SET and GET operations at modest scale
|
|
284
|
+
- many TTL expirations without protocol failure
|
|
285
|
+
- SCAN on a non-trivial keyspace
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## 23. Package Scripts
|
|
290
|
+
|
|
291
|
+
Suggested package scripts:
|
|
292
|
+
|
|
293
|
+
```json
|
|
294
|
+
{
|
|
295
|
+
"type": "module",
|
|
296
|
+
"scripts": {
|
|
297
|
+
"test": "node --test",
|
|
298
|
+
"test:unit": "node --test test/unit",
|
|
299
|
+
"test:integration": "node --test test/integration",
|
|
300
|
+
"test:contract": "node --test test/contract",
|
|
301
|
+
"test:stress": "node --test test/stress",
|
|
302
|
+
"test:all": "node --test test"
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## 24. Acceptance Criteria for Initial Milestone
|
|
310
|
+
|
|
311
|
+
The first meaningful milestone is achieved when the server can be exercised successfully through `redis-cli` and passes automated tests for the following sequence:
|
|
312
|
+
|
|
313
|
+
```text
|
|
314
|
+
PING
|
|
315
|
+
SET foo bar
|
|
316
|
+
GET foo
|
|
317
|
+
EXPIRE foo 10
|
|
318
|
+
TTL foo
|
|
319
|
+
DEL foo
|
|
320
|
+
HSET user:1 name Martin age 42
|
|
321
|
+
HGET user:1 name
|
|
322
|
+
HGETALL user:1
|
|
323
|
+
SADD tags a b c
|
|
324
|
+
SMEMBERS tags
|
|
325
|
+
TYPE foo
|
|
326
|
+
SCAN 0
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
In addition, the system must satisfy these conditions:
|
|
330
|
+
|
|
331
|
+
- persistence survives restart
|
|
332
|
+
- wrong-type errors are returned correctly
|
|
333
|
+
- expired keys behave as missing keys
|
|
334
|
+
- binary-safe values do not break command semantics
|
|
335
|
+
- multiple clients can connect and issue commands without corrupting state
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## 25. Implementation Order
|
|
340
|
+
|
|
341
|
+
Recommended phased implementation order:
|
|
342
|
+
|
|
343
|
+
### Phase 1
|
|
344
|
+
|
|
345
|
+
Infrastructure: TCP server, RESP parser/encoder, command registry, SQLite init, error model, protocol tests.
|
|
346
|
+
|
|
347
|
+
### Phase 2
|
|
348
|
+
|
|
349
|
+
Strings and core: PING, ECHO, GET, SET, DEL, EXISTS, TYPE.
|
|
350
|
+
|
|
351
|
+
### Phase 3
|
|
352
|
+
|
|
353
|
+
Expiration: EXPIRE, PEXPIRE, TTL, PTTL, PERSIST, lazy expiration, active sweeper.
|
|
354
|
+
|
|
355
|
+
### Phase 4
|
|
356
|
+
|
|
357
|
+
Numeric string operations: INCR, DECR, INCRBY, DECRBY.
|
|
358
|
+
|
|
359
|
+
### Phase 5
|
|
360
|
+
|
|
361
|
+
Hashes: HSET, HGET, HMGET, HGETALL, HDEL, HEXISTS, HINCRBY.
|
|
362
|
+
|
|
363
|
+
### Phase 6
|
|
364
|
+
|
|
365
|
+
Sets: SADD, SREM, SMEMBERS, SISMEMBER, SCARD.
|
|
366
|
+
|
|
367
|
+
### Phase 7
|
|
368
|
+
|
|
369
|
+
Introspection and observability: SCAN, SQLITE.INFO, CACHE.INFO.
|
|
370
|
+
|
|
371
|
+
### Phase 8
|
|
372
|
+
|
|
373
|
+
Migration tooling: programmatic Redis migration API using SCAN, import verification and reporting.
|
|
374
|
+
|
|
375
|
+
### Phase 9
|
|
376
|
+
|
|
377
|
+
Search extension: FT.CREATE, FT.SEARCH, FT.INFO, FT.DROPINDEX (SQLite FTS5 with BM25).
|
|
378
|
+
|
|
379
|
+
Search should only begin after the core command, TTL, persistence, and concurrency suites are stable.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# RESPLite Specification v1 — Migration Strategy (Core)
|
|
2
|
+
|
|
3
|
+
## 26. Migration Strategy from Redis
|
|
4
|
+
|
|
5
|
+
Migration is not part of the RESP protocol surface in v1.
|
|
6
|
+
It should be implemented as a programmatic module outside the RESP command surface.
|
|
7
|
+
|
|
8
|
+
### 26.1 Initial migration method
|
|
9
|
+
|
|
10
|
+
Recommended initial approach:
|
|
11
|
+
|
|
12
|
+
- connect to a real Redis instance
|
|
13
|
+
- iterate keys using `SCAN`
|
|
14
|
+
- discover key type with `TYPE`
|
|
15
|
+
- fetch values according to type
|
|
16
|
+
- fetch expiration using `PTTL`
|
|
17
|
+
- write translated data into SQLite
|
|
18
|
+
|
|
19
|
+
### 26.2 Initial migratable subset
|
|
20
|
+
|
|
21
|
+
The initial migration module should support:
|
|
22
|
+
|
|
23
|
+
- strings
|
|
24
|
+
- hashes
|
|
25
|
+
- sets
|
|
26
|
+
- TTL metadata
|
|
27
|
+
|
|
28
|
+
### 26.3 Not required initially
|
|
29
|
+
|
|
30
|
+
- RDB parsing
|
|
31
|
+
- AOF parsing
|
|
32
|
+
- mirror mode
|
|
33
|
+
- dual-write migration
|
|
34
|
+
|
|
35
|
+
These can be considered later if adoption demands them.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 27. Compatibility Matrix Guidance
|
|
40
|
+
|
|
41
|
+
The README should include a compatibility matrix with at least three groups:
|
|
42
|
+
|
|
43
|
+
### Supported
|
|
44
|
+
|
|
45
|
+
All commands implemented in v1 (and any extensions such as lists, zsets, FT.*, blocking, migration API).
|
|
46
|
+
|
|
47
|
+
### Planned
|
|
48
|
+
|
|
49
|
+
Future near-term commands or features as documented in the respective spec files.
|
|
50
|
+
|
|
51
|
+
### Not Supported
|
|
52
|
+
|
|
53
|
+
Features explicitly excluded from the roadmap for now, such as:
|
|
54
|
+
|
|
55
|
+
- Pub/Sub
|
|
56
|
+
- Streams
|
|
57
|
+
- Lua
|
|
58
|
+
- Cluster
|
|
59
|
+
- Replication
|
|
60
|
+
- Blocking commands (unless added per blocking spec)
|
|
61
|
+
|
|
62
|
+
This matrix is essential for setting correct user expectations.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 28. Final Design Rule
|
|
67
|
+
|
|
68
|
+
A command should only be added if its implementation is:
|
|
69
|
+
|
|
70
|
+
- natural on SQLite
|
|
71
|
+
- semantically clear
|
|
72
|
+
- testable
|
|
73
|
+
- operationally safe
|
|
74
|
+
- maintainable without excessive special handling
|
|
75
|
+
|
|
76
|
+
If a command requires too much implementation gymnastics to preserve Redis-like behavior, it should not be part of the supported surface until a clean design exists.
|
|
77
|
+
|
|
78
|
+
This rule is central to the project.
|
|
79
|
+
It protects correctness, keeps the scope honest, and preserves the identity of RESPLite as a practical Redis-compatible server built on top of SQLite rather than a fragile imitation.
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# RESPLite — Type: Lists (Appendix B)
|
|
2
|
+
|
|
3
|
+
## B.1 Goals
|
|
4
|
+
|
|
5
|
+
* Provide Redis-compatible LIST commands over RESP2, backed by SQLite persistence.
|
|
6
|
+
* Avoid O(n) reindexing for push/pop operations.
|
|
7
|
+
* Support binary-safe values (`Buffer`) and keys as `BLOB`.
|
|
8
|
+
* Maintain correct Redis semantics for empty lists and wrong-type errors.
|
|
9
|
+
|
|
10
|
+
## B.2 Supported Commands (vNext)
|
|
11
|
+
|
|
12
|
+
* `LPUSH key value [value ...]`
|
|
13
|
+
* `RPUSH key value [value ...]`
|
|
14
|
+
* `LPOP key [count]`
|
|
15
|
+
* `RPOP key [count]`
|
|
16
|
+
* `LLEN key`
|
|
17
|
+
* `LRANGE key start stop`
|
|
18
|
+
* `LINDEX key index` (optional, recommended)
|
|
19
|
+
* `LSET key index value` (optional, later)
|
|
20
|
+
* `LTRIM key start stop` (optional, later)
|
|
21
|
+
|
|
22
|
+
**Not supported initially**
|
|
23
|
+
|
|
24
|
+
* Blocking commands: `BLPOP`, `BRPOP`, `BRPOPLPUSH`
|
|
25
|
+
* `RPOPLPUSH`, `LMOVE` (later if needed)
|
|
26
|
+
|
|
27
|
+
## B.3 Redis Semantics
|
|
28
|
+
|
|
29
|
+
* **Wrong type:** If `key` exists and is not a list, return:
|
|
30
|
+
|
|
31
|
+
* `WRONGTYPE Operation against a key holding the wrong kind of value`
|
|
32
|
+
* **Non-existent key:**
|
|
33
|
+
|
|
34
|
+
* `LLEN` returns `0`
|
|
35
|
+
* `LRANGE` returns empty array
|
|
36
|
+
* `LPOP/RPOP` returns `nil` (or empty array for count > 1)
|
|
37
|
+
* **Empty list after removals:** When a list becomes empty, delete the logical key:
|
|
38
|
+
|
|
39
|
+
* delete metadata from `redis_keys`
|
|
40
|
+
* delete list meta/items rows
|
|
41
|
+
|
|
42
|
+
## B.4 Data Model (SQLite)
|
|
43
|
+
|
|
44
|
+
Lists are stored using a monotonic sequence strategy per key to avoid shifting indices.
|
|
45
|
+
|
|
46
|
+
### B.4.1 Metadata table
|
|
47
|
+
|
|
48
|
+
```sql
|
|
49
|
+
CREATE TABLE redis_list_meta (
|
|
50
|
+
key BLOB PRIMARY KEY,
|
|
51
|
+
head_seq INTEGER NOT NULL,
|
|
52
|
+
tail_seq INTEGER NOT NULL,
|
|
53
|
+
FOREIGN KEY(key) REFERENCES redis_keys(key) ON DELETE CASCADE
|
|
54
|
+
);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Invariant:**
|
|
58
|
+
|
|
59
|
+
* Empty list should not exist (no meta row). If it exists, it must satisfy `head_seq <= tail_seq`.
|
|
60
|
+
|
|
61
|
+
### B.4.2 Items table
|
|
62
|
+
|
|
63
|
+
```sql
|
|
64
|
+
CREATE TABLE redis_list_items (
|
|
65
|
+
key BLOB NOT NULL,
|
|
66
|
+
seq INTEGER NOT NULL,
|
|
67
|
+
value BLOB NOT NULL,
|
|
68
|
+
PRIMARY KEY (key, seq),
|
|
69
|
+
FOREIGN KEY(key) REFERENCES redis_keys(key) ON DELETE CASCADE
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
CREATE INDEX redis_list_items_key_seq_idx ON redis_list_items(key, seq);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## B.5 Sequence Strategy
|
|
76
|
+
|
|
77
|
+
* Each list key maintains:
|
|
78
|
+
|
|
79
|
+
* `head_seq`: the smallest sequence currently used (front)
|
|
80
|
+
* `tail_seq`: the largest sequence currently used (back)
|
|
81
|
+
* For a new list:
|
|
82
|
+
|
|
83
|
+
* initialize `head_seq = 0`, `tail_seq = -1` (or similar empty sentinel)
|
|
84
|
+
* but we recommend **not creating meta** until first push.
|
|
85
|
+
|
|
86
|
+
### Push operations
|
|
87
|
+
|
|
88
|
+
* `LPUSH`:
|
|
89
|
+
|
|
90
|
+
* decrement `head_seq` and insert new items at `seq = head_seq - 1` per pushed element
|
|
91
|
+
* `RPUSH`:
|
|
92
|
+
|
|
93
|
+
* increment `tail_seq` and insert new items at `seq = tail_seq + 1`
|
|
94
|
+
|
|
95
|
+
Maintain ordering:
|
|
96
|
+
|
|
97
|
+
* front is smaller `seq`, back is larger `seq`.
|
|
98
|
+
|
|
99
|
+
## B.6 Command Behavior
|
|
100
|
+
|
|
101
|
+
### B.6.1 LPUSH
|
|
102
|
+
|
|
103
|
+
**Request:** `LPUSH key v1 v2 ...`
|
|
104
|
+
**Response:** Integer = new list length
|
|
105
|
+
|
|
106
|
+
Implementation notes:
|
|
107
|
+
|
|
108
|
+
* If key does not exist: create `redis_keys` type list + create `redis_list_meta`.
|
|
109
|
+
* Insert values in left-push order consistent with Redis:
|
|
110
|
+
|
|
111
|
+
* `LPUSH mylist a b c` results in list `[c, b, a]` at the head.
|
|
112
|
+
* Use a single transaction:
|
|
113
|
+
|
|
114
|
+
* ensure type
|
|
115
|
+
* upsert meta
|
|
116
|
+
* insert items
|
|
117
|
+
* bump `redis_keys.version`, update `updated_at`
|
|
118
|
+
|
|
119
|
+
### B.6.2 RPUSH
|
|
120
|
+
|
|
121
|
+
Same structure, response is new length.
|
|
122
|
+
|
|
123
|
+
### B.6.3 LLEN
|
|
124
|
+
|
|
125
|
+
Return length:
|
|
126
|
+
|
|
127
|
+
* If key not exist: `0`
|
|
128
|
+
* Else length = `tail_seq - head_seq + 1` (derived from meta)
|
|
129
|
+
* Do not count rows for performance.
|
|
130
|
+
|
|
131
|
+
### B.6.4 LPOP / RPOP
|
|
132
|
+
|
|
133
|
+
* Without `count`: return bulk string or `nil`
|
|
134
|
+
* With `count`:
|
|
135
|
+
|
|
136
|
+
* Redis returns array of popped elements
|
|
137
|
+
* If list has fewer than count: return all available
|
|
138
|
+
* If key does not exist: return `nil` for no-count, or empty array for count
|
|
139
|
+
* After popping last element: delete key and meta.
|
|
140
|
+
|
|
141
|
+
**Implementation:**
|
|
142
|
+
|
|
143
|
+
* In a transaction:
|
|
144
|
+
|
|
145
|
+
* read meta
|
|
146
|
+
* compute seq range to remove
|
|
147
|
+
* select values for response
|
|
148
|
+
* delete those rows
|
|
149
|
+
* update meta head/tail accordingly
|
|
150
|
+
* if empty -> delete key meta entirely
|
|
151
|
+
|
|
152
|
+
### B.6.5 LRANGE
|
|
153
|
+
|
|
154
|
+
`LRANGE key start stop` returns array of elements.
|
|
155
|
+
|
|
156
|
+
Index rules (Redis):
|
|
157
|
+
|
|
158
|
+
* `start` and `stop` are inclusive.
|
|
159
|
+
* Negative indices count from end (`-1` is last element).
|
|
160
|
+
* Clamp indices to valid bounds.
|
|
161
|
+
* If start > stop after normalization: empty array.
|
|
162
|
+
|
|
163
|
+
**Mapping indices to sequences:**
|
|
164
|
+
|
|
165
|
+
* Let `len = tail_seq - head_seq + 1`
|
|
166
|
+
* Normalize start/stop into `[0..len-1]`
|
|
167
|
+
* Convert to seq:
|
|
168
|
+
|
|
169
|
+
* `seq_start = head_seq + start`
|
|
170
|
+
* `seq_stop = head_seq + stop`
|
|
171
|
+
* SQL:
|
|
172
|
+
|
|
173
|
+
* `SELECT value FROM redis_list_items WHERE key=? AND seq BETWEEN ? AND ? ORDER BY seq ASC`
|
|
174
|
+
|
|
175
|
+
## B.7 Expiration and Cache
|
|
176
|
+
|
|
177
|
+
* TTL is stored in `redis_keys.expires_at`, same as other types.
|
|
178
|
+
* Lazy expiration must delete list meta/items like other types.
|
|
179
|
+
* Cache strategy (recommended):
|
|
180
|
+
|
|
181
|
+
* Cache small lists (len <= configured threshold) optionally.
|
|
182
|
+
* Otherwise cache only meta (`head_seq`, `tail_seq`) if beneficial.
|
|
183
|
+
* Always invalidate on list writes.
|
|
184
|
+
|
|
185
|
+
## B.8 Complexity Targets
|
|
186
|
+
|
|
187
|
+
* `LPUSH/RPUSH`: O(k) inserts, no reindexing
|
|
188
|
+
* `LPOP/RPOP`: O(k) deletes + selects for return
|
|
189
|
+
* `LLEN`: O(1)
|
|
190
|
+
* `LRANGE`: O(n) in returned range
|
|
191
|
+
|
|
192
|
+
## B.9 Tests (Required)
|
|
193
|
+
|
|
194
|
+
For each command above:
|
|
195
|
+
|
|
196
|
+
* Happy path
|
|
197
|
+
* Non-existent key behavior
|
|
198
|
+
* Wrong-type behavior
|
|
199
|
+
* TTL interaction (expires then acts as missing)
|
|
200
|
+
* Persistence across restart
|
|
201
|
+
* Binary safety for values
|
|
202
|
+
* Concurrency sanity (multiple clients pushing/popping does not corrupt meta)
|