diff --git a/doc/source/index.rst b/doc/source/index.rst
index 478dc6b1f..cc87813c9 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -73,6 +73,7 @@ NeuG documentation
:caption: Transaction
transaction/transaction.md
+ transaction/checkpoint.md
.. toctree::
:maxdepth: 1
diff --git a/doc/source/reference/cpp_api/neug_db.md b/doc/source/reference/cpp_api/neug_db.md
index 1b6576621..a7eb3d58e 100644
--- a/doc/source/reference/cpp_api/neug_db.md
+++ b/doc/source/reference/cpp_api/neug_db.md
@@ -59,10 +59,20 @@ Open the database from persistent storage.
Initializes and opens the NeuG database from the specified data directory. This method loads the graph schema, vertex/edge data, and initializes the query processor and planner.
-**Data Directory Structure:** The data_dir should contain:
-- `graph.yaml`: `Schema` definition file
-- `snapshot/`: Vertex and edge data files
-- `wal/`: Write-ahead log files (optional, for recovery)
+**Data Directory Structure:** Persistent state is organized into numbered checkpoint generations:
+
+```text
+data_dir/
+├── checkpoint-N/
+│ ├── meta
+│ ├── snapshot/
+│ ├── runtime/
+│ ├── allocator/
+│ └── wal/
+└── checkpoint-(N+1).next/ # transient staging generation, when present
+```
+
+NeuG opens the highest valid published generation. A `.next` directory is not visible as the current checkpoint and is cleaned up during writable recovery if checkpoint creation did not complete.
**Usage Example:**
```cpp
@@ -135,7 +145,8 @@ db.Close(); // Persist data and cleanup
- **Notes:**
- This method is idempotent - calling it multiple times is safe.
- - After closing, the database cannot be reopened. Create a new `NeugDB` instance to open the database again.
+ - After closing, the same `NeugDB` instance can be opened again.
+ - Checkpoint-on-close is best effort. If it fails, `Close()` logs an error, suppresses the exception, and continues releasing resources.
- **Since:** v0.1.0
diff --git a/doc/source/reference/nodejs_api/database.md b/doc/source/reference/nodejs_api/database.md
index cbadf9a42..a88ded9c0 100644
--- a/doc/source/reference/nodejs_api/database.md
+++ b/doc/source/reference/nodejs_api/database.md
@@ -169,5 +169,7 @@ Connect to the database asynchronously.
close()
```
-Close the database connection and release all resources.
+Close the database and release all resources.
All open connections and async connections will be closed automatically.
+
+For a read-write database with `checkpointOnClose: true`, this method creates a checkpoint before releasing the database resources. Automatic checkpoint creation is best effort: failures are logged and do not propagate to the caller.
diff --git a/doc/source/reference/python_api/database.md b/doc/source/reference/python_api/database.md
index 692d88b7d..bbdaccbb6 100644
--- a/doc/source/reference/python_api/database.md
+++ b/doc/source/reference/python_api/database.md
@@ -231,7 +231,11 @@ Connect to the database asynchronously.
def close()
```
-Close the database connection.
+Close the database and all of its connections.
+
+For a read-write database with `checkpoint_on_close=True`, this method creates a checkpoint before releasing the database resources.
+The method is idempotent.
+Automatic checkpoint creation is best effort: failures are logged and do not propagate to the caller.
diff --git a/doc/source/transaction/_meta.ts b/doc/source/transaction/_meta.ts
new file mode 100644
index 000000000..f5c42407e
--- /dev/null
+++ b/doc/source/transaction/_meta.ts
@@ -0,0 +1,4 @@
+export default {
+ transaction: "Transaction Management",
+ checkpoint: "Checkpoints",
+};
diff --git a/doc/source/transaction/checkpoint.md b/doc/source/transaction/checkpoint.md
new file mode 100644
index 000000000..0af5ada18
--- /dev/null
+++ b/doc/source/transaction/checkpoint.md
@@ -0,0 +1,176 @@
+# Checkpoints
+
+A checkpoint creates a durable snapshot of the current database state. The
+two deployment modes persist writes differently: Embedded mode holds changes
+in memory until a checkpoint writes them to disk, while Service mode appends
+every committed change to a write-ahead log (WAL). A checkpoint therefore
+plays a different role in each mode:
+
+| Question | Embedded mode | Service mode |
+|---|---|---|
+| Is `CHECKPOINT` required for durability? | Yes; un-checkpointed changes are lost when the database closes | No; committed changes are already durable in the WAL |
+| What is it for? | Persist changes and create a recovery point | Optional maintenance: fold WAL records into a fresh snapshot |
+| What is recovered after a restart? | The last successful checkpoint | The last checkpoint plus WAL replay |
+
+For transaction boundaries and concurrency outside checkpoint operations, see
+[Transaction Management](transaction.md).
+
+## Run a checkpoint
+
+```cypher
+CHECKPOINT;
+```
+
+`CHECKPOINT` takes no arguments and runs under the `update` access mode.
+Omit `access_mode` and NeuG infers `update`, or set it explicitly:
+
+```python
+conn.execute("CHECKPOINT")
+conn.execute("CHECKPOINT", access_mode="update") # or "u"
+```
+
+Any other explicit mode (`read`/`r`, `insert`/`i`, or `schema`/`s`) is
+rejected, and a database opened read-only cannot create a checkpoint.
+
+- `EXPLAIN CHECKPOINT` returns the execution plan without creating a
+ checkpoint.
+- `PROFILE CHECKPOINT` creates the checkpoint and reports its execution time
+ as a single `Checkpoint` operator.
+
+### Embedded mode example
+
+```python
+import neug
+
+# This application checkpoints explicitly and checks the result of each
+# call, so it disables the automatic checkpoint on close.
+db = neug.Database("/path/to/database", checkpoint_on_close=False)
+conn = db.connect()
+
+conn.execute("COPY Person FROM 'people.csv'")
+conn.execute("CHECKPOINT") # The loaded data is now durable.
+
+conn.close()
+db.close()
+```
+
+### Service mode example
+
+```python
+from neug import Session
+
+session = Session("http://localhost:10000/")
+
+# This insert is durable as soon as it commits; CHECKPOINT is not required.
+session.execute(
+ "CREATE (p:Person {name: 'Alice'})",
+ access_mode="insert",
+)
+
+# Optional maintenance: fold committed WAL records into a fresh snapshot.
+session.execute("CHECKPOINT")
+session.close()
+```
+
+Closing a client `Session` only disconnects that client; it neither closes
+nor checkpoints the server database.
+
+## What a checkpoint does
+
+Each successful checkpoint performs the following steps:
+
+1. Take exclusive control of the database, waiting for in-flight work to
+ finish (see [Concurrency](#concurrency)).
+2. Write a complete new snapshot generation to disk, alongside the current
+ one.
+3. Publish the new generation atomically. From this point on it is the
+ recovery point; the previous generation is no longer needed for recovery.
+4. Reopen the live database on the new generation and resume normal
+ operation. In Service mode this also starts a fresh, empty WAL.
+5. Remove the retired generation (best effort).
+
+Because step 2 writes a full copy while the current generation still exists,
+peak disk usage during a checkpoint is roughly twice the database size, and
+checkpoint time grows with database size. Disk usage drops back once the
+retired generation is removed in step 5. Run checkpoints as often as your
+recovery-point and WAL-size requirements justify, rather than on a fixed
+tight interval.
+
+### Concurrency
+
+- **Embedded mode:** A checkpoint takes the exclusive query lock. It waits
+ for running operations to finish and blocks new operations until it
+ completes.
+- **Service mode:** A checkpoint waits for in-flight reads and writes to
+ finish without interrupting them, holds off new transactions while it
+ runs, and then executes with no concurrent transactions. The wait is
+ unbounded: a single long-running query can delay the entire checkpoint.
+ After a successful checkpoint, existing sessions remain valid and NeuG
+ starts a new empty WAL.
+
+For Service mode, schedule checkpoints during a quiet period when possible.
+
+## Automatic checkpoint on close
+
+In the Python API, persistent read-write databases default to
+`checkpoint_on_close=True`, so closing the database attempts a final
+checkpoint. This automatic checkpoint is best effort: NeuG logs a failure
+but does not raise it from `close()`.
+
+Use an explicit `CHECKPOINT` when the application must know whether
+persistence succeeded. If you set `checkpoint_on_close=False`:
+
+- Embedded mode discards changes made after the last successful checkpoint
+ when the database closes.
+- Service mode can recover committed changes from the WAL, even if they were
+ made after the last checkpoint.
+
+Checkpoints do not make a temporary in-memory database survive after it is
+closed.
+
+## Failure and recovery
+
+On startup, NeuG opens the newest completely published checkpoint; an
+incomplete generation is never selected. In Service mode, NeuG then replays
+WAL records created after that checkpoint.
+
+A manual `CHECKPOINT` fails in one of two ways:
+
+- **Before the snapshot build starts** — for example, if the database cannot
+ begin maintenance — the statement returns an error and the database keeps
+ running normally.
+- **After the snapshot build has started**, a failure can leave the
+ in-memory state undefined, so NeuG deliberately terminates the database
+ process rather than continue. Restart the database to recover from the
+ latest published checkpoint and, in Service mode, the WAL.
+
+### Recovering from a failed Embedded write
+
+Large Embedded-mode writes are not fully atomic. To make a failed batch
+discardable, disable checkpoint on close, create a recovery point before the
+batch, and checkpoint again only after the batch succeeds:
+
+```python
+import neug
+
+path = "/path/to/database"
+db = neug.Database(path, checkpoint_on_close=False)
+conn = db.connect()
+
+conn.execute("CHECKPOINT") # Recovery point before the batch
+
+try:
+ conn.execute("COPY Person FROM 'large_batch.csv'")
+ conn.execute("CHECKPOINT") # Persist the completed batch
+except Exception:
+ # Do not checkpoint the possibly partial in-memory state.
+ conn.close()
+ db.close()
+
+ db = neug.Database(path, checkpoint_on_close=False)
+ conn = db.connect() # Reopens the last successful checkpoint
+```
+
+For long imports, checkpoint after each batch whose completed work is worth
+preserving. Always leave enough temporary disk space for a new full
+generation.
diff --git a/doc/source/transaction/transaction.md b/doc/source/transaction/transaction.md
index 0c645b24f..0231a7f45 100644
--- a/doc/source/transaction/transaction.md
+++ b/doc/source/transaction/transaction.md
@@ -48,18 +48,9 @@ Embedded mode is designed for analytical workloads where simplicity and single-q
For statements involving substantial data writes (such as `COPY` for bulk loading), a failure mid-execution may result in partial data being written to memory. This is a known trade-off for AP workloads where the overhead of full rollback mechanisms is typically unnecessary.
-**Practical Workaround:** Since all writes are held in memory until an explicit `CHECKPOINT`, users can establish recovery points:
-
-```cypher
--- Create a recovery point before large operations
-CHECKPOINT;
-
--- Perform bulk data loading
-COPY Person FROM 'large_dataset.csv';
-
--- If an error occurs before this point, restart the database
--- to reload from the last checkpoint (all uncommitted changes are discarded)
-```
+In Embedded mode, checkpoints can be used as recovery points around large
+write operations. See [Checkpoints](checkpoint.md) for a safe recovery example
+and the interaction with `checkpoint_on_close`.
#### Consistency
@@ -83,23 +74,7 @@ Changes are persisted to disk **only** after:
- Database closure with `checkpoint_on_close=True` (default)
For in-memory databases, durability is not applicable as data exists only in volatile memory.
-
-```python
-import neug
-
-db = neug.Database("/path/to/database")
-conn = db.connect()
-
-# Changes are in memory only
-conn.execute("CREATE (p:Person {name: 'Alice'})")
-
-# Explicit persistence to disk
-conn.execute("CHECKPOINT")
-
-# Or rely on automatic checkpoint at close
-conn.close()
-db.close() # checkpoint_on_close=True by default
-```
+For usage and failure behavior, see [Checkpoints](checkpoint.md).
### Service Mode
@@ -123,7 +98,7 @@ NeuG uses **Multi-Version Concurrency Control (MVCC)** to provide serializable i
| Insert | Concurrent with reads and other inserts. Waits if an update is in progress. |
| Update | One at a time. Reads continue on a consistent snapshot. Inserts and other updates wait until the update finishes. |
| Schema (DDL) | Same concurrency behavior as update. |
-| Checkpoint | Same concurrency behavior as update. |
+| Checkpoint | Exclusive maintenance. See [Checkpoints](checkpoint.md). |
> Note: A read that arrives while an update is finishing may wait briefly,
> typically at millisecond scale.
@@ -131,7 +106,8 @@ NeuG uses **Multi-Version Concurrency Control (MVCC)** to provide serializable i
**Design Rationale:** This hybrid approach reflects the reality of graph workloads:
- **Reads and inserts** are the dominant operations in most graph applications (social networks, knowledge graphs, recommendation systems)
-- **Updates, schema changes, and checkpoints** are relatively rare and are serialized, while reads continue on a consistent snapshot with only brief waits when they arrive as a write is finishing
+- **Updates and schema changes** are relatively rare and are serialized, while reads continue on a consistent snapshot with only brief waits when they arrive as a write is finishing
+- **Checkpoints** are database-level maintenance operations; their concurrency behavior is described in [Checkpoints](checkpoint.md)
- Full MVCC for all write types would add significant complexity with minimal benefit for typical graph workloads
```python
@@ -156,7 +132,7 @@ All modifications are immediately persisted through **Write-Ahead Logging (WAL)*
- Changes are logged to WAL before execution completes
- Crash recovery automatically replays WAL to restore consistent state
-- No explicit checkpoint required for durability (but available for WAL consolidation)
+- No explicit checkpoint is required for durability; checkpoints are available for WAL consolidation. See [Checkpoints](checkpoint.md)
## Access Mode
@@ -171,24 +147,22 @@ When executing queries, you can specify an `access_mode` to control transaction
**Default Behavior:** If not specified, NeuG infers the appropriate mode. Explicitly specifying the correct mode enables better concurrency optimization in Service mode.
-**Access Mode Hierarchy:** Access modes follow a hierarchy where higher modes provide stronger guarantees but lower concurrency:
-
-```
-read < insert < update
-```
-
-`schema` uses the same serialized update path as `update`.
-
-- **Upward compatibility**: Using a higher access mode than required always works (e.g., `update` mode for a read-only query), but may reduce throughput due to stronger locking
-- **Downward restriction**: Using a lower access mode than required causes an error (e.g., `read` mode for an insert operation)
+**Access Mode Capabilities:** Access modes are operation-specific rather than a strict hierarchy. In Service mode, `read` and `insert` are narrow modes, while `update` and `schema` use the general serialized update path. Using a mode that does not support the planned operation returns an error.
| Specified Mode | Actual Operation | Result |
|----------------|------------------|--------|
| `read` | read | ✅ OK |
-| `read` | insert/update | ❌ Error |
-| `insert` | read/insert | ✅ OK |
-| `insert` | update/schema | ❌ Error |
-| `update`/`schema` | any | ✅ OK (serialized update path) |
+| `read` | insert/update/schema/checkpoint | ❌ Error |
+| `insert` | insert | ✅ OK |
+| `insert` | read/update/schema/checkpoint | ❌ Error |
+| `update` | read/insert/update/schema/checkpoint | ✅ OK (serialized update path) |
+| `schema` | read/insert/update/schema | ✅ OK (serialized update path) |
+
+In Embedded mode, modes other than `read` primarily select shared versus exclusive query locking rather than distinct transaction implementations. Explicit `read` still rejects write operations, and the special `CHECKPOINT` restriction below applies in both deployment modes.
+
+`CHECKPOINT` is a special administrative statement. See
+[Checkpoints](checkpoint.md) for its accepted access modes and
+`EXPLAIN`/`PROFILE` behavior.
```python
# Optimal: match access mode to operation for best concurrency
@@ -203,33 +177,8 @@ conn.execute("MATCH (n) RETURN n", access_mode="update") # works, but reduc
### Embedded Mode: Checkpoint-Based
-All changes are held in memory until explicitly persisted:
-
-```python
-import neug
-
-db = neug.Database("/path/to/database")
-conn = db.connect()
-
-# Establish recovery point
-conn.execute("CHECKPOINT")
-
-# Perform operations (changes in memory)
-conn.execute("COPY Person FROM 'employees.csv'")
-conn.execute("COPY Company FROM 'companies.csv'")
-
-# Persist all changes to disk
-conn.execute("CHECKPOINT")
-
-conn.close()
-db.close()
-```
-
-**Checkpoint Characteristics:**
-- Creates an atomic snapshot of the current database state
-- Blocks other operations during execution
-- May require significant time for large datasets
-- Replaces the previous checkpoint atomically
+All changes are held in memory until a checkpoint persists them. See
+[Checkpoints](checkpoint.md) for usage, examples, and recovery behavior.
### Service Mode: WAL-Based
@@ -244,46 +193,18 @@ session = Session("http://localhost:10000/")
session.execute("CREATE (p:Person {name: 'Alice'})") # Persisted via WAL
session.execute("CREATE (p:Person {name: 'Bob'})") # Persisted via WAL
-# Optional: consolidate WAL into checkpoint for storage optimization
-session.execute("CHECKPOINT")
-
session.close()
```
-**Service Mode Checkpoint:**
-- Does not block active reads
-- Briefly waits for in-flight inserts before starting
-- New reads and inserts wait briefly during checkpoint finalization
-- Consolidates WAL entries into a unified checkpoint
-- Clears processed WAL entries to reclaim storage
-- Does not affect the automatic durability of individual statements
+An optional checkpoint consolidates the WAL but is not required for statement
+durability. See [Checkpoints](checkpoint.md).
## Error Recovery
### Embedded Mode
-Recovery relies on the last successful checkpoint:
-
-```python
-import neug
-
-db = neug.Database("/path/to/database")
-conn = db.connect()
-
-conn.execute("CHECKPOINT") # Recovery point
-
-try:
- conn.execute("COPY LargeTable FROM 'huge_file.csv'")
- conn.execute("CHECKPOINT") # Commit if successful
-except Exception as e:
- # Partial data may be in memory
- # Close and reopen to restore from last checkpoint
- conn.close()
- db.close()
-
- db = neug.Database("/path/to/database") # Restores from checkpoint
- conn = db.connect()
-```
+Recovery relies on the last successful checkpoint. See
+[Recovering from a failed Embedded write](checkpoint.md) for an example.
### Service Mode
@@ -357,9 +278,9 @@ finally:
|----------|-------------------|-------------------|
| **Atomicity** | Partial (checkpoint-based recovery) | Full (automatic rollback) |
| **Consistency** | Schema constraints enforced | Schema constraints enforced |
-| **Isolation** | Exclusive write locks | MVCC for reads/inserts; serialized updates/DDL/checkpoints |
+| **Isolation** | Exclusive write locks | MVCC for reads/inserts; serialized updates/DDL; exclusive checkpoints |
| **Durability** | Explicit CHECKPOINT or close | Automatic WAL persistence |
-| **Concurrent Reads** | Yes | Yes |
+| **Concurrent Reads** | Yes | Yes, except during exclusive checkpoint maintenance |
| **Concurrent Inserts** | No | Yes, unless an update is active |
| **Concurrent Updates** | No | No, updates are serialized while reads continue on consistent snapshots |
| **Recovery** | Manual (checkpoint reload) | Automatic (WAL replay) |
diff --git a/include/neug/compiler/gopt/g_physical_analyzer.h b/include/neug/compiler/gopt/g_physical_analyzer.h
index 3db560159..33d384e72 100644
--- a/include/neug/compiler/gopt/g_physical_analyzer.h
+++ b/include/neug/compiler/gopt/g_physical_analyzer.h
@@ -47,7 +47,7 @@ struct ExecutionFlag {
bool schema = false;
bool batch = false;
bool create_temp_table = false;
- bool transaction = false;
+ bool checkpoint = false;
bool procedure_call = false;
};
@@ -253,7 +253,9 @@ class GPhysicalAnalyzer {
break;
}
case planner::LogicalOperatorType::TRANSACTION: {
- flag.transaction = true;
+ auto transaction = op.constPtrCast();
+ flag.checkpoint = transaction->getTransactionAction() ==
+ transaction::TransactionAction::CHECKPOINT;
break;
}
// set read to true for graph operators
diff --git a/include/neug/compiler/gopt/g_physical_convertor.h b/include/neug/compiler/gopt/g_physical_convertor.h
index ce6c012d8..491064bbf 100644
--- a/include/neug/compiler/gopt/g_physical_convertor.h
+++ b/include/neug/compiler/gopt/g_physical_convertor.h
@@ -67,7 +67,7 @@ class GPhysicalConvertor {
flagPB->set_schema(flag.schema);
flagPB->set_batch(flag.batch);
flagPB->set_create_temp_table(flag.create_temp_table);
- flagPB->set_checkpoint(flag.transaction);
+ flagPB->set_checkpoint(flag.checkpoint);
flagPB->set_procedure_call(flag.procedure_call);
return flagPB;
}
diff --git a/include/neug/compiler/planner/graph_planner.h b/include/neug/compiler/planner/graph_planner.h
index 73583e29c..fe6e87945 100644
--- a/include/neug/compiler/planner/graph_planner.h
+++ b/include/neug/compiler/planner/graph_planner.h
@@ -49,7 +49,7 @@ class IGraphPlanner {
// Attempts to infer the execution access mode from the given query.
// The current implementation relies on static analysis of the query string
- // and can only distinguish between "update" and "read" modes.
+ // and can distinguish read, update, and schema modes.
// Inferring an "insert" mode would require more complex operator
// combination analysis, which is not supported at the moment.
virtual AccessMode analyzeMode(const std::string& query) const = 0;
diff --git a/include/neug/execution/common/params_map.h b/include/neug/execution/common/params_map.h
index 7fbf82ecf..efd19732f 100644
--- a/include/neug/execution/common/params_map.h
+++ b/include/neug/execution/common/params_map.h
@@ -14,6 +14,8 @@
*/
#pragma once
+#include
+
#include