Implement an in-memory database with TTL and backup
Company: xAI
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: easy
Interview Round: Take-home Project
## In-Memory Database (Levels 1–4: TTL and Backup/Restore)
Implement an in-memory database that stores **records** identified by a **string key**. Each record contains multiple **string field → string value** pairs.
You must support a set of operations that progressively add features.
### Data model
- `key` is a string.
- Each `key` maps to a set of fields.
- Each `field` maps to a `value` (both strings).
If an operation refers to a missing `key` or `field`, treat it as absent.
> Assumption to make outputs well-defined (typical for OAs):
> - `get*` returns `""` (empty string) when absent.
> - `delete*` returns `true` if something was deleted, else `false`.
> - `scan*` returns an empty list when nothing matches.
---
## Level 1: Basic CRUD on fields
Implement:
- `set(key, field, value)`
- `get(key, field) -> string`
- `delete(key, field) -> bool`
`set` inserts or overwrites the field’s value.
---
## Level 2: Read-only listing
Implement:
- `scan(key) -> list[string]`
- `scan_by_prefix(key, prefix) -> list[string]`
Return format:
- Each returned element is formatted as `"field(value)"`.
- Results are sorted **lexicographically by `field`**.
- `scan_by_prefix` returns only fields whose name starts with `prefix`.
---
## Level 3: Timestamped operations + TTL
Add timestamped variants of the above operations. Tests will use **either** timestamped APIs **or** non-timestamped APIs, but **never mix them**.
All timestamped operations accept an integer `timestamp`.
Implement:
- `set_at(key, field, value, timestamp)`
- `set_at_with_ttl(key, field, value, timestamp, ttl)`
- `get_at(key, field, timestamp) -> string`
- `delete_at(key, field, timestamp) -> bool`
- `scan_at(key, timestamp) -> list[string]`
- `scan_by_prefix_at(key, prefix, timestamp) -> list[string]`
TTL semantics:
- `set_at_with_ttl` makes the field valid over the half-open interval:
- **valid in** `[timestamp, timestamp + ttl)`
- Expired fields must **not** appear in `get_at`, `scan_at`, or prefix scans.
- Time always moves forward: timestamps provided to operations are **non-decreasing**.
---
## Level 4: Backup and restore
Implement:
- `backup(timestamp)`
- `restore(timestamp, timestamp_to_restore)`
Backup requirements:
- `backup(t)` stores a snapshot of the database state at time `t`.
- For fields with TTL, the backup must capture **remaining TTL** at backup time (i.e., how much lifetime is left at `t`).
Restore requirements:
- `restore(now, timestamp_to_restore)` restores the database from the **latest** backup whose backup time is **≤ `timestamp_to_restore`**.
- After restoring at current time `now`, TTL expiration must be **recalculated** based on remaining TTL stored in the backup:
- If a field had remaining TTL `r` in the backup, then after restore at time `now` it should expire at `now + r`.
- Fields that were already expired at the moment of backup should not be present in that backup.
Your implementation should correctly handle overwrites, deletions, scans, TTL expiry, and backup/restore interactions under the monotonic-time guarantee.
Quick Answer: This question evaluates proficiency in designing and implementing mutable in-memory data structures, field-level CRUD and scanning, timestamped operations with TTL semantics, and backup/restore snapshot management, focusing on data structures, temporal reasoning, and state management in the Coding & Algorithms domain.
Part 1: Basic CRUD on In-Memory Database Fields
Implement a simple **in-memory key-value store** that supports basic CRUD operations on record fields, then return the results of all read/delete queries.
Each **record** is identified by a string `key`, and every key owns a collection of **`field -> value`** pairs, where both `field` and `value` are strings.
## Function
```python
def solution(queries):
```
`queries` is a list of operations. Each operation is itself a list whose first element is the operation name. Process the operations **in order** and return a list containing the output of every `GET` and `DELETE` operation, in the order they occur.
## Operations
- **`['SET', key, field, value]`** — Insert the `field -> value` pair under `key`. If the key does not yet exist, create it. If the field already exists, **overwrite** its value. Produces **no output**.
- **`['GET', key, field]`** — Return the stored value for `field` under `key`. If the key does not exist, or the key exists but the field does not, return the **empty string `''`**. Appends one value to the output.
- **`['DELETE', key, field]`** — Remove `field` from `key`. Return `True` if the field existed and was removed; otherwise return `False`. Appends one boolean to the output.
## Output
Return a list with one entry per `GET` and per `DELETE`, in query order. `SET` operations contribute nothing, so a run containing only `SET` operations returns an empty list `[]`.
## Examples
**Example 1**
```
queries = [
['SET', 'user1', 'name', 'alice'],
['GET', 'user1', 'name'],
['SET', 'user1', 'name', 'bob'],
['GET', 'user1', 'name'],
['DELETE', 'user1', 'name'],
['GET', 'user1', 'name'],
['DELETE', 'user1', 'name'],
]
```
Output: `['alice', 'bob', True, '', False]`
- `GET user1.name` → `'alice'`
- After the second `SET`, `GET user1.name` → `'bob'` (overwritten)
- `DELETE user1.name` → `True` (field existed)
- `GET user1.name` → `''` (field no longer exists)
- `DELETE user1.name` → `False` (already gone)
**Example 2**
```
queries = [['GET', 'missing', 'x'], ['DELETE', 'missing', 'x']]
```
Output: `['', False]` — reading or deleting from a key that was never set returns `''` and `False` respectively.
**Example 3**
```
queries = [
['SET', 'a', 'x', '1'],
['SET', 'a', 'y', '2'],
['DELETE', 'a', 'x'],
['GET', 'a', 'x'],
['GET', 'a', 'y'],
]
```
Output: `[True, '', '2']` — deleting one field of a key leaves the key's other fields intact.
## Constraints
- `0 <= len(queries) <= 100000`
- Each `key`, `field`, and `value` is a string of length 1 to 100.
- Only the operations `'SET'`, `'GET'`, and `'DELETE'` appear.
- You may assume average **O(1)** hash-map operations.
Constraints
- 0 <= len(queries) <= 100000
- Each key, field, and value is a string of length 1 to 100
- Only the operations 'SET', 'GET', and 'DELETE' appear
- Average O(1) hash-map operations may be assumed
Examples
Input: [['SET', 'user1', 'name', 'alice'], ['GET', 'user1', 'name'], ['SET', 'user1', 'name', 'bob'], ['GET', 'user1', 'name'], ['DELETE', 'user1', 'name'], ['GET', 'user1', 'name'], ['DELETE', 'user1', 'name']]
Expected Output: ['alice', 'bob', True, '', False]
Explanation: The value is overwritten from 'alice' to 'bob'. The first delete succeeds, and the second delete fails because the field is already gone.
Input: [['GET', 'missing', 'x'], ['DELETE', 'missing', 'x']]
Expected Output: ['', False]
Explanation: Missing keys and fields are treated as absent.
Hints
- A nested dictionary works well: one map from key to another map of field to value.
- After deleting a field, you may remove the key entirely if it has no fields left.
Part 2: Sorted Scan and Prefix Scan in an In-Memory Database
Implement an **in-memory database** that stores fields per record and supports point operations plus two ordered scans.
Implement the function:
```python
def solution(queries):
```
## Data model
The database holds **records**. Each record is identified by a string **key** and stores a collection of **field → value** pairs, where both `field` and `value` are strings. Setting an existing field overwrites its value (last write wins).
## Input
- **`queries`** — a list of operations, processed in order. Each operation is itself a list of strings whose first element is the operation name, followed by its arguments:
| Operation | Form | Behavior | Output |
|---|---|---|---|
| **SET** | `['SET', key, field, value]` | Create the record if needed, then set/overwrite `field` to `value`. | *(none — produces no output)* |
| **GET** | `['GET', key, field]` | Look up the value of `field` in `key`. | The stored value string, or `''` if the key or field does not exist. |
| **DELETE** | `['DELETE', key, field]` | If both the key and field exist, remove that field (and remove the record entirely once it has no remaining fields). | `True` if a field was removed, otherwise `False`. |
| **SCAN** | `['SCAN', key]` | Read all fields of `key`. | A list of strings `'field(value)'`, **sorted lexicographically by field name**. |
| **SCAN_BY_PREFIX** | `['SCAN_BY_PREFIX', key, prefix]` | Same as SCAN, but only include fields whose names **start with** `prefix`. | A list of strings `'field(value)'`, **sorted lexicographically by field name**. |
## Output
Return a list containing the outputs of **every query except `SET`**, in the order the queries were processed. Each non-`SET` query contributes exactly one entry:
- `GET` → a value string (`''` when absent)
- `DELETE` → a boolean (`True`/`False`)
- `SCAN` / `SCAN_BY_PREFIX` → a (possibly empty) list of `'field(value)'` strings
## Scan formatting and edge cases
- Each matched field is formatted as the literal `field` name, followed by its `value` wrapped in parentheses: **`'field(value)'`**.
- Results of `SCAN` and `SCAN_BY_PREFIX` are always **sorted lexicographically by field name** before formatting.
- A scan on a **missing key**, or a `SCAN_BY_PREFIX` where **no field matches** the prefix, returns an **empty list** `[]`.
## Example
```text
queries = [
['SET', 'user', 'b', '2'],
['SET', 'user', 'a', '1'],
['SCAN', 'user'],
['SCAN_BY_PREFIX', 'user', 'a'],
]
```
Output: `[['a(1)', 'b(2)'], ['a(1)']]`
The two `SET`s produce no output. `SCAN 'user'` lists both fields sorted by name. `SCAN_BY_PREFIX 'user' 'a'` lists only the field starting with `'a'`.
## Constraints
- `0 <= len(queries) <= 100000`
- Each key, field, value, and prefix is a string of length `0` to `100`.
- Results for scan operations must be sorted lexicographically by field name.
- Average **O(1)** hash-map operations may be assumed.
Constraints
- 0 <= len(queries) <= 100000
- Each key, field, value, and prefix is a string of length 0 to 100
- Results for scan operations must be sorted lexicographically by field name
- Average O(1) hash-map operations may be assumed
Examples
Input: [['SET', 'user', 'b', '2'], ['SET', 'user', 'a', '1'], ['SCAN', 'user'], ['SCAN_BY_PREFIX', 'user', 'a']]
Expected Output: [['a(1)', 'b(2)'], ['a(1)']]
Explanation: Fields are returned in lexicographic order, and the prefix scan keeps only fields starting with 'a'.
Input: [['SET', 'k', 'name', 'Ann'], ['DELETE', 'k', 'age'], ['GET', 'k', 'age'], ['SCAN_BY_PREFIX', 'k', 'z'], ['SCAN', 'missing']]
Expected Output: [False, '', [], []]
Explanation: Deleting a missing field returns False. A missing field returns '', and scans with no matches return empty lists.
Hints
- Keep the same nested dictionary structure from basic CRUD.
- For a scan, collect matching field names first, sort them, then build the 'field(value)' strings.
Part 3: Timestamped In-Memory Database with TTL
Implement a **timestamped in-memory key-value database** that supports an optional **time-to-live (TTL)** on individual fields.
Each top-level **key** maps to a set of **field → value** pairs, where both fields and values are strings. Every operation carries an explicit timestamp, and you must answer each query as of that timestamp.
## Function
Implement:
```python
def solution(queries):
...
```
`queries` is a list of operations. Each operation is itself a list of strings whose first element is the operation name. Process the operations **in the given order** and return a single list containing the results of the operations that produce output (defined below), in the same order they occur.
## Operations
All numeric arguments (timestamps and TTLs) are given as **decimal strings** and should be interpreted as integers.
| Operation | Argument layout | Returns |
|-----------|-----------------|---------|
| `SET_AT` | `["SET_AT", key, field, value, timestamp]` | nothing |
| `SET_AT_WITH_TTL` | `["SET_AT_WITH_TTL", key, field, value, timestamp, ttl]` | nothing |
| `GET_AT` | `["GET_AT", key, field, timestamp]` | the value string, or `""` |
| `DELETE_AT` | `["DELETE_AT", key, field, timestamp]` | `True` or `False` |
| `SCAN_AT` | `["SCAN_AT", key, timestamp]` | a list of strings |
| `SCAN_BY_PREFIX_AT` | `["SCAN_BY_PREFIX_AT", key, prefix, timestamp]` | a list of strings |
Only `GET_AT`, `DELETE_AT`, `SCAN_AT`, and `SCAN_BY_PREFIX_AT` contribute to the returned list. `SET_AT` and `SET_AT_WITH_TTL` produce no output.
### Semantics
- **`SET_AT`** — Set `field = value` under `key` at `timestamp`, with **no expiry**. If the field already exists (including one previously set with a TTL), it is overwritten and any prior TTL is removed.
- **`SET_AT_WITH_TTL`** — Set `field = value` under `key` at `timestamp` with the given `ttl`. The field is **valid during the half-open interval `[timestamp, timestamp + ttl)`** — i.e. it exists at and after `timestamp` but is treated as **absent once the current time reaches `timestamp + ttl`**. As with `SET_AT`, this overwrites any existing value/TTL for that field.
- **`GET_AT`** — Return the value of `field` under `key` as of `timestamp`. Return `""` (empty string) if the key or field does not exist or the field has expired.
- **`DELETE_AT`** — Delete `field` under `key` as of `timestamp`. Return `True` if a **live (non-expired) field was actually present and removed**; return `False` if the key/field does not exist or the field has already expired.
- **`SCAN_AT`** — Return all live (non-expired) fields under `key` as of `timestamp`, formatted as `"field(value)"`, **sorted lexicographically by field name**. Return an empty list if the key does not exist or has no live fields.
- **`SCAN_BY_PREFIX_AT`** — Same as `SCAN_AT`, but include only fields whose name **starts with `prefix`**. Results are likewise formatted as `"field(value)"` and sorted lexicographically by field name.
## Expiry rules
- An expired field behaves exactly as if it were **absent** for all subsequent gets, deletes, and scans.
- The timestamps supplied across the queries are **non-decreasing**, so once a field has expired it never becomes valid again.
## Output
Return the list of results for the output-producing operations, in query order. For example, a `GET_AT` that finds a live value contributes that value string; a `SCAN_AT` contributes its (possibly empty) sorted list; a `DELETE_AT` contributes `True` or `False`.
## Constraints
- `0 <= len(queries) <= 100000`
- All timestamps are decimal strings representing integers in `[0, 10^9]`.
- All TTL values are positive decimal strings representing integers in `[1, 10^9]`.
- The current timestamps in the queries are non-decreasing.
Constraints
- 0 <= len(queries) <= 100000
- All timestamps are decimal strings representing integers in [0, 10^9]
- All ttl values are positive decimal strings representing integers in [1, 10^9]
- The current timestamps in the queries are non-decreasing
Examples
Input: [['SET_AT_WITH_TTL', 'k', 'a', 'x', '10', '5'], ['GET_AT', 'k', 'a', '10'], ['GET_AT', 'k', 'a', '14'], ['GET_AT', 'k', 'a', '15'], ['DELETE_AT', 'k', 'a', '15']]
Expected Output: ['x', 'x', '', False]
Explanation: The field is valid at times 10 through 14, but not at 15 because the TTL interval is [10, 15).
Input: [['SET_AT', 'u', 'name', 'Ann', '1'], ['SET_AT_WITH_TTL', 'u', 'age', '20', '2', '3'], ['SCAN_AT', 'u', '3'], ['SET_AT', 'u', 'age', '21', '4'], ['GET_AT', 'u', 'age', '6'], ['SCAN_BY_PREFIX_AT', 'u', 'a', '6']]
Expected Output: [['age(20)', 'name(Ann)'], '21', ['age(21)']]
Explanation: The TTL version of 'age' is alive at time 3. It is overwritten at time 4 by a non-TTL value, so it still exists at time 6.
Hints
- Store an absolute expiration time like expire_at = timestamp + ttl instead of storing ttl directly.
- Because time never goes backward, you can lazily remove expired fields whenever a key is touched.
Part 4: Backup and Restore in a Timestamped Database with TTL
Implement a **timestamped, in-memory key–value database** that supports **per-field TTL (time-to-live)** plus **backup** and **restore** operations.
The database is a two-level store: each **key** maps to a set of **fields**, and each field holds a string **value**. A field may optionally have a TTL, after which it expires and is treated as if it never existed. All operations carry an explicit timestamp, so behavior is fully deterministic.
## Function
```python
def solution(queries):
...
```
- **`queries`** is a list of operations. Each operation is a list of strings: the operation name followed by its arguments.
- Return a list containing the result of every operation that produces output (`GET_AT`, `DELETE_AT`, `SCAN_AT`, `SCAN_BY_PREFIX_AT`), in the order those operations appear. `SET_AT`, `SET_AT_WITH_TTL`, `BACKUP`, and `RESTORE` produce **no** output.
All numeric arguments (timestamps, TTLs, restore arguments) arrive as decimal strings and should be parsed as integers.
## Operations
**`["SET_AT", key, field, value, timestamp]`**
Set `field` of `key` to `value` with no expiry. Overwrites any existing value for that field. No output.
**`["SET_AT_WITH_TTL", key, field, value, timestamp, ttl]`**
Set `field` of `key` to `value` with a TTL of `ttl`, so the field expires at `timestamp + ttl`. Overwrites any existing value. No output.
**`["GET_AT", key, field, timestamp]`**
Return the current `value` of `field` under `key` as a string. If the field does not exist or has expired at `timestamp`, return the empty string `""`.
**`["DELETE_AT", key, field, timestamp]`**
Delete `field` from `key`. Return `True` if a live (non-expired) field was present and removed at `timestamp`; otherwise return `False`.
**`["SCAN_AT", key, timestamp]`**
Return a list of strings for all live fields under `key`, **sorted ascending by field name**, each formatted as `"field(value)"`. If the key has no live fields, return an empty list.
**`["SCAN_BY_PREFIX_AT", key, prefix, timestamp]`**
Same as `SCAN_AT`, but only include fields whose name **starts with** `prefix`. Results are sorted ascending by field name and formatted as `"field(value)"`.
**`["BACKUP", timestamp]`**
Take a snapshot of the entire live database at `timestamp`. No output.
**`["RESTORE", now, timestamp_to_restore]`**
Replace the entire database with a previously saved snapshot. No output.
## Expiry rule
A field with expiry time `E` is **live** while `current_time < E` and **expired** once `current_time >= E`. Expired fields must never be returned by `GET_AT`, counted by `DELETE_AT`, or listed by either scan operation, and must never be included in a backup.
## Backup semantics
- A `BACKUP` at time `t` snapshots only the fields that are **live** at `t`; already-expired fields are excluded.
- For each TTL field, the snapshot stores the field's **remaining** TTL (`E - t`), **not** its absolute expiry time. Fields without a TTL are stored as non-expiring.
## Restore semantics
`RESTORE` is given two timestamps: the current time `now`, and a `timestamp_to_restore` (which may point to any past time).
- Choose the **latest backup whose backup time is `<= timestamp_to_restore`**.
- Restore that snapshot as the live database, treating `now` as the moment of restoration. Each stored field's remaining TTL `r` is converted back into an absolute expiry of `now + r`, so the field expires `r` units after the restore. Fields without a TTL remain non-expiring.
- If **no** backup has a time `<= timestamp_to_restore`, restore to an **empty** database.
## Ordering guarantees
The current-time argument used by `SET_AT`, `SET_AT_WITH_TTL`, `GET_AT`, `DELETE_AT`, `SCAN_AT`, `SCAN_BY_PREFIX_AT`, `BACKUP`, and the `now` argument of `RESTORE` are **non-decreasing** across the query sequence. Only `timestamp_to_restore` may refer to an arbitrary past time.
## Constraints
- `0 <= len(queries) <= 100000`
- All timestamps, TTL values, and restore arguments are decimal strings representing integers in `[0, 10^9]`.
- All TTL values are positive.
Constraints
- 0 <= len(queries) <= 100000
- All timestamps, ttl values, and restore arguments are decimal strings representing integers in [0, 10^9]
- All ttl values are positive
- The current time arguments for SET/GET/DELETE/SCAN/BACKUP and RESTORE-now are non-decreasing
- timestamp_to_restore may point to any past time
Examples
Input: [['SET_AT_WITH_TTL', 'doc', 'a', '1', '10', '5'], ['BACKUP', '12'], ['GET_AT', 'doc', 'a', '13'], ['RESTORE', '20', '12'], ['SCAN_AT', 'doc', '22'], ['GET_AT', 'doc', 'a', '23']]
Expected Output: ['1', ['a(1)'], '']
Explanation: At backup time 12, the field has 3 units of TTL left. After restoring at time 20, it is alive at 22 and expires at 23.
Input: [['SET_AT', 'k', 'x', 'A', '1'], ['BACKUP', '2'], ['SET_AT', 'k', 'x', 'B', '3'], ['BACKUP', '4'], ['DELETE_AT', 'k', 'x', '5'], ['RESTORE', '6', '3'], ['GET_AT', 'k', 'x', '6'], ['RESTORE', '7', '100'], ['GET_AT', 'k', 'x', '7']]
Expected Output: [True, 'A', 'B']
Explanation: Restoring to time 3 uses the backup from time 2, so the value is 'A'. Restoring to time 100 uses the later backup from time 4, so the value is 'B'.
Hints
- At backup time t, store remaining_ttl = expire_at - t for live TTL fields.
- Since backups are taken in time order, you can keep them in a list and use binary search to find the latest backup with time <= timestamp_to_restore.