CSharpDB REST API Reference

The CSharpDB REST API is now hosted by CSharpDB.Daemon by default. The daemon exposes the existing HTTP /api surface and the gRPC service from one long-running process backed by the same warm database client.

The REST surface enables cross-language interoperability over JSON/HTTP. Built with ASP.NET Core Minimal APIs, it includes OpenAPI documentation and an interactive Scalar UI when the daemon runs in Development mode.

Running the API

Run the combined daemon host from source:

dotnet run --project src/CSharpDB.Daemon/CSharpDB.Daemon.csproj

The daemon launch profile starts on https://localhost:49995 and http://localhost:49996. REST endpoints are available under /api, and gRPC endpoints are available from the same host.

Interactive documentation: In Development mode, open https://localhost:49995/scalar/v1 or http://localhost:49996/scalar/v1 in a browser to explore and test endpoints with the Scalar API explorer.

For a stable local HTTP port, set ASPNETCORE_URLS explicitly before starting the daemon:

$env:ConnectionStrings__CSharpDB = "Data Source=C:\data\sample.db"
$env:ASPNETCORE_URLS = "http://localhost:5820"
dotnet run --project src/CSharpDB.Daemon/CSharpDB.Daemon.csproj

With that override, the REST base URL is http://localhost:5820/api and Scalar is available at http://localhost:5820/scalar/v1.

Configuration

The current default database path and daemon host behavior are configured in src/CSharpDB.Daemon/appsettings.json:

{
  "ConnectionStrings": {
    "CSharpDB": "Data Source=csharpdb.db"
  },
  "CSharpDB": {
    "Daemon": {
      "EnableRestApi": true
    },
    "HostDatabase": {
      "OpenMode": "HybridIncrementalDurable",
      "ImplicitInsertExecutionMode": "ConcurrentWriteTransactions",
      "UseWriteOptimizedPreset": true,
      "HotTableNames": [],
      "HotCollectionNames": []
    }
  }
}

CSharpDB:Daemon:EnableRestApi controls whether the daemon maps the REST /api surface. The default is true. Set it to false only when the daemon should expose gRPC without REST.

CORS is enabled for all origins by default (development convenience). JSON responses use camelCase naming and omit null values.

The standalone CSharpDB.Api project remains available for REST-only hosting, but the recommended remote host is CSharpDB.Daemon so REST and gRPC clients share the same warm database process.

Endpoints

All endpoints are prefixed with /api.

Database Info

GET /api/info

Returns a summary of the database.

Response:

{
  "dataSource": "csharpdb.db",
  "tableCount": 3,
  "indexCount": 2,
  "viewCount": 1,
  "triggerCount": 1,
  "procedureCount": 2
}

Storage Inspection

Read-only physical diagnostics endpoints for .db and .wal inspection.

GET /api/inspect

Run a full database file inspection.

Query parameters:

  • includePages (default: false) — include per-page decoded details in the response
  • path (optional) — override database path for this request

GET /api/inspect/wal

Inspect WAL header/frame/checksum state.

Query parameters:

  • path (optional) — override database path for this request

GET /api/inspect/page/{id}

Inspect a single page by page id.

Query parameters:

  • hex (default: false) — include page hex dump
  • path (optional) — override database path for this request

GET /api/inspect/indexes

Validate index metadata and root tree reachability.

Query parameters:

  • index (optional) — check one index by name
  • sample (optional) — sample size hint for future index validation passes
  • path (optional) — override database path for this request

Responses follow the diagnostics models documented in Storage Inspector.

Maintenance

POST /api/maintenance/migrate-foreign-keys

Validate or apply foreign-key retrofit migration for older databases whose tables do not yet persist FK metadata.

Request:

{
  "validateOnly": true,
  "backupDestinationPath": "pre-fk.backup.db",
  "violationSampleLimit": 100,
  "constraints": [
    {
      "tableName": "orders",
      "columnName": "customer_id",
      "referencedTableName": "customers",
      "referencedColumnName": "id",
      "onDelete": "Restrict"
    }
  ]
}

Response:

{
  "validateOnly": true,
  "succeeded": false,
  "backupDestinationPath": null,
  "affectedTables": 1,
  "appliedForeignKeys": 1,
  "copiedRows": 0,
  "violationCount": 1,
  "violations": [
    {
      "tableName": "orders",
      "columnName": "customer_id",
      "referencedTableName": "customers",
      "referencedColumnName": "id",
      "childKeyColumnName": "id",
      "childKeyValue": 42,
      "childValue": 999,
      "reason": "MissingReferencedParent"
    }
  ],
  "appliedConstraints": [
    {
      "tableName": "orders",
      "columnName": "customer_id",
      "referencedTableName": "customers",
      "referencedColumnName": "id",
      "constraintName": "fk_orders_customer_id_abcd1234",
      "supportingIndexName": "__fk_orders_customer_id_abcd1234",
      "onDelete": "Restrict"
    }
  ]
}

Notes:

  • validateOnly = true previews the migration without mutating schema or data.
  • backupDestinationPath is optional and is only used during apply mode.
  • Paths are resolved on the daemon host machine, not on the caller.

Tables

GET /api/tables

List all table names.

Response:

["users", "orders", "products"]

GET /api/tables/{name}/schema

Get the full schema for a table.

Response:

{
  "tableName": "users",
  "columns": [
    { "name": "id", "type": "Integer", "nullable": false, "isPrimaryKey": true, "isIdentity": true },
    { "name": "name", "type": "Text", "nullable": false, "isPrimaryKey": false, "isIdentity": false },
    { "name": "age", "type": "Integer", "nullable": true, "isPrimaryKey": false, "isIdentity": false }
  ]
}

GET /api/tables/{name}/count

Get the row count for a table.

Response:

{ "count": 42 }

DELETE /api/tables/{name}

Drop a table.

Response: 200 OK with { "message": "Table 'users' dropped." }

PATCH /api/tables/{name}/rename

Rename a table.

Request:

{ "newName": "customers" }

Response: 200 OK with { "message": "Table renamed from 'users' to 'customers'." }

POST /api/tables/{name}/columns

Add a column to a table.

Request:

{ "columnName": "email", "type": "TEXT", "notNull": false }

Response: 200 OK with { "message": "Column 'email' added to 'users'." }

DELETE /api/tables/{name}/columns/{col}

Drop a column.

Response: 200 OK with { "message": "Column 'email' dropped from 'users'." }

PATCH /api/tables/{name}/columns/{col}/rename

Rename a column.

Request:

{ "newName": "full_name" }

Response: 200 OK with { "message": "Column renamed from 'name' to 'full_name' in 'users'." }

Rows

GET /api/tables/{name}/rows

Browse table rows with pagination.

Query parameters:

  • page (default: 1) — Page number
  • pageSize (default: 50, max: 1000) — Rows per page

Response:

{
  "tableName": "users",
  "page": 1,
  "pageSize": 50,
  "totalRows": 3,
  "columns": ["id", "name", "age"],
  "rows": [
    { "id": 1, "name": "Alice", "age": 30 },
    { "id": 2, "name": "Bob", "age": 25 }
  ]
}

GET /api/tables/{name}/rows/{pkValue}

Get a single row by primary key.

Query parameters:

  • pkColumn (default: "id") — Name of the primary key column

Response:

{ "id": 1, "name": "Alice", "age": 30 }

POST /api/tables/{name}/rows

Insert a new row.

Request:

{ "values": { "id": 4, "name": "Diana", "age": 28 } }

Response: 201 Created with { "message": "Row inserted into 'users'.", "rowsAffected": 1 }

PUT /api/tables/{name}/rows/{pkValue}

Update a row by primary key.

Query parameters:

  • pkColumn (default: "id") — Name of the primary key column

Request:

{ "values": { "name": "Diana Updated", "age": 29 } }

Response: 200 OK with { "message": "Row updated in 'users'.", "rowsAffected": 1 }

DELETE /api/tables/{name}/rows/{pkValue}

Delete a row by primary key.

Query parameters:

  • pkColumn (default: "id") — Name of the primary key column

Response: 200 OK with { "message": "Row deleted from 'users'.", "rowsAffected": 1 }

Indexes

GET /api/indexes

List all indexes.

Response:

[
  { "name": "idx_category", "tableName": "products", "columnName": "category", "isUnique": false }
]

POST /api/indexes

Create an index.

Request:

{ "indexName": "idx_email", "tableName": "users", "columnName": "email", "isUnique": true }

Response: 201 Created with { "message": "Index 'idx_email' created." }

PUT /api/indexes/{name}

Update (drop and recreate) an index.

Request:

{ "newIndexName": "idx_user_email", "tableName": "users", "columnName": "email", "isUnique": true }

Response: 200 OK

DELETE /api/indexes/{name}

Drop an index.

Response: 200 OK with { "message": "Index 'idx_email' dropped." }

Views

GET /api/views

List all views.

Response:

["order_summary", "product_catalog"]

GET /api/views/{name}

Get a view definition.

Response:

{
  "viewName": "order_summary",
  "selectSql": "SELECT o.id, c.name, o.total FROM orders o INNER JOIN customers c ON o.customer_id = c.id"
}

GET /api/views/{name}/rows

Browse view results with pagination.

Query parameters:

  • page (default: 1)
  • pageSize (default: 50, max: 1000)

Response: Same shape as table browse (columns + rows).

POST /api/views

Create a view.

Request:

{ "viewName": "expensive_products", "selectSql": "SELECT name, price FROM products WHERE price > 50" }

Response: 201 Created

PUT /api/views/{name}

Update a view (drop and recreate).

Request:

{ "newViewName": "expensive_products", "selectSql": "SELECT name, price FROM products WHERE price > 100" }

Response: 200 OK

DELETE /api/views/{name}

Drop a view.

Response: 200 OK

Triggers

GET /api/triggers

List all triggers.

Response:

[
  {
    "name": "trg_update_stock",
    "tableName": "order_items",
    "timing": "After",
    "event": "Insert",
    "bodySql": "UPDATE products SET stock = stock - NEW.quantity WHERE id = NEW.product_id"
  }
]

POST /api/triggers

Create a trigger.

Request:

{
  "triggerName": "trg_audit_insert",
  "tableName": "users",
  "timing": "After",
  "event": "Insert",
  "bodySql": "INSERT INTO audit_log VALUES ('INSERT', NEW.name)"
}

Timing values: "Before", "After"

Event values: "Insert", "Update", "Delete"

Response: 201 Created

PUT /api/triggers/{name}

Update a trigger (drop and recreate).

Response: 200 OK

DELETE /api/triggers/{name}

Drop a trigger.

Response: 200 OK

SQL Execution

POST /api/sql/execute

Execute an arbitrary SQL statement.

Request:

{ "sql": "SELECT name, price FROM products WHERE price > 10 ORDER BY price DESC" }

Response (query):

{
  "isQuery": true,
  "columnNames": ["name", "price"],
  "rows": [
    { "name": "Widget", "price": 29.99 },
    { "name": "Gadget", "price": 14.99 }
  ],
  "rowsAffected": 0,
  "elapsed": 1.23
}

Response (mutation):

{
  "isQuery": false,
  "columnNames": null,
  "rows": null,
  "rowsAffected": 3,
  "elapsed": 0.87
}

Procedures

Table-backed procedure catalog (__procedures) with strict parameter metadata validation and transactional execution.

GET /api/procedures

List procedure metadata.

GET /api/procedures/{name}

Get one procedure definition.

POST /api/procedures

Create a procedure.

Request:

{
  "name": "GetUserById",
  "bodySql": "SELECT * FROM users WHERE id = @id;",
  "parameters": [
    { "name": "id", "type": "INTEGER", "required": true, "default": null, "description": "User ID" }
  ],
  "description": "Lookup user by ID",
  "isEnabled": true
}

PUT /api/procedures/{name}

Update (or rename) a procedure.

Request:

{
  "newName": "GetUserById",
  "bodySql": "SELECT * FROM users WHERE id = @id;",
  "parameters": [
    { "name": "id", "type": "INTEGER", "required": true }
  ],
  "description": "Updated description",
  "isEnabled": true
}

DELETE /api/procedures/{name}

Delete a procedure.

POST /api/procedures/{name}/execute

Execute a stored procedure by name.

Request:

{
  "args": {
    "id": 123
  }
}

Response (success):

{
  "procedureName": "GetUserById",
  "succeeded": true,
  "statements": [
    {
      "statementIndex": 0,
      "statementText": "SELECT * FROM users WHERE id = @id;",
      "isQuery": true,
      "columnNames": ["id", "name"],
      "rows": [{ "id": 123, "name": "Alice" }],
      "rowsAffected": 1,
      "elapsedMs": 0.34
    }
  ],
  "error": null,
  "failedStatementIndex": null,
  "elapsedMs": 0.51
}

Response (validation/runtime failure): 400 Bad Request with the same shape and succeeded = false.

Error Handling

The API uses standard HTTP status codes and returns structured error responses:

HTTP StatusCSharpDB Error CodeMeaning
400SyntaxError, TypeMismatchBad request — invalid SQL or type mismatch
404TableNotFound, ColumnNotFoundResource not found
409DuplicateKey, TableAlreadyExistsConflict — duplicate resource
422ConstraintViolationConstraint violated (NOT NULL, UNIQUE)
503BusyDatabase is busy (another writer is active)
500(other)Unexpected server error

Error response format:

{
  "error": "Table 'nonexistent' not found.",
  "code": "TableNotFound"
}

In development mode, a detail field with a stack trace is included.

See Also