An open API service indexing awesome lists of open source software.

https://github.com/theeightbot/tychodb

The death of traditional SQLite sparks the new life of SQLite as an object store
https://github.com/theeightbot/tychodb

csharp database json nosql

Last synced: about 1 month ago
JSON representation

The death of traditional SQLite sparks the new life of SQLite as an object store

Awesome Lists containing this project

README

          

# TychoDB


TychoDB Logo

TychoDB is a high-performance .NET library that provides a simple and efficient way to store and retrieve JSON objects using SQLite. It's designed to be a lightweight and fast database solution for .NET applications, with a focus on ease of use and flexibility.

![License](https://img.shields.io/github/license/TheEightBot/TychoDB)
![NuGet](https://img.shields.io/nuget/v/TychoDB)

## Features

- **Simple API**: Intuitive methods for storing, retrieving, and querying JSON data
- **Type Registration**: Flexible registration of C# types with custom ID selectors
- **Advanced Querying**: Rich filtering and sorting capabilities for complex data retrievals
- **Partitioning**: Organize your data using logical partitions
- **Binary Data Support**: Store and retrieve binary large objects (BLOBs)
- **Indexing**: Create indexes on properties for faster query performance
- **Encryption**: Optional AES-256 full-database encryption via SQLCipher (`TychoDB.Encrypted`)
- **Multiple Serialization Options**: Support for System.Text.Json, Newtonsoft.Json, and MessagePack
- **Asynchronous Operations**: Full async/await support for all database operations
- **LINQ-like Syntax**: Familiar querying patterns for .NET developers
- **Nested Object Support**: Query and filter on nested object properties
- **Optimized Performance**: Efficient memory usage and connection management

## Installation

Install TychoDB via NuGet:

```bash
dotnet add package TychoDB
```

Depending on your preferred JSON serializer, you can also install one of the following:

```bash
dotnet add package TychoDB.JsonSerializer.SystemTextJson
dotnet add package TychoDB.JsonSerializer.NewtonsoftJson
```

If you need **full database encryption** (powered by SQLCipher), use the encrypted variant instead of the standard package:

```bash
dotnet add package TychoDB.Encrypted
```

## Encryption

TychoDB supports full database encryption via [SQLCipher](https://www.zetetic.net/sqlcipher/). Encryption is provided through a separate NuGet package that replaces the standard SQLite driver with the SQLCipher-backed variant.

### Installing the Encrypted Package

Instead of the standard `TychoDB` package, install `TychoDB.Encrypted`:

```bash
dotnet add package TychoDB.Encrypted
```

> **Note:** `TychoDB` and `TychoDB.Encrypted` are mutually exclusive — only reference one of them in a given project.

### Creating an Encrypted Database

Pass a `password` to the `Tycho` constructor. When a password is provided the underlying SQLCipher driver automatically encrypts the entire database file using AES-256.

```csharp
using TychoDB;

var jsonSerializer = new SystemTextJsonSerializer();

using var db = new Tycho(
dbPath: "./data",
jsonSerializer: jsonSerializer,
password: "your-strong-password")
.Connect();
```

All subsequent reads and writes are transparently encrypted/decrypted — the rest of the API is identical to the non-encrypted version.

### Opening an Existing Encrypted Database

Supply the same password that was used when the database was first created:

```csharp
using var db = new Tycho(
dbPath: "./data",
jsonSerializer: jsonSerializer,
dbName: "tycho_cache.db",
password: "your-strong-password")
.Connect();
```

Providing the wrong password (or no password) will cause a `SqliteException` when the connection is opened.

### Security Considerations

- Store the password securely — use platform secret stores (e.g. Android Keystore, iOS Keychain, Windows DPAPI, or a secrets manager) rather than hard-coding it.
- The `TychoDB.Encrypted` package pulls in `SQLitePCLRaw.bundle_e_sqlcipher`, which links against the SQLCipher native library. Ensure your target platforms are supported by SQLCipher.
- Changing the password of an existing database requires re-keying (outside the scope of the TychoDB API); use raw SQLCipher `PRAGMA rekey` if needed.

## Quick Start

Here's a simple example to get you started:

```csharp
using TychoDB;
using System;
using System.Threading.Tasks;

// Define a class to store
public class Person
{
public string Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public DateTime DateOfBirth { get; set; }
}

public class Program
{
public static async Task Main()
{
// Create a JSON serializer (System.Text.Json implementation)
var jsonSerializer = new SystemTextJsonSerializer();

// Initialize Tycho and connect to a database
using var db = new Tycho("./data", jsonSerializer)
.Connect();

// Create a person object
var person = new Person
{
Id = "123",
Name = "John Doe",
Age = 30,
DateOfBirth = new DateTime(1992, 5, 15)
};

// Write the object to the database
await db.WriteObjectAsync(person, x => x.Id);

// Read the object back by its key
var retrievedPerson = await db.ReadObjectAsync("123");

Console.WriteLine($"Retrieved: {retrievedPerson.Name}, Age: {retrievedPerson.Age}");
}
}
```

## Type Registration

TychoDB provides several ways to register your types, which helps with ID selection and comparison:

```csharp
// Register a type with a specific ID property
db.AddTypeRegistration(x => x.Id);

// Register using a custom key selector function
db.AddTypeRegistrationWithCustomKeySelector(x => $"{x.Id}_{x.Name}");

// Register using convention-based ID property detection
db.AddTypeRegistration();
```

After registration, you can use simplified write/read operations:

```csharp
// Write without specifying a key selector
await db.WriteObjectAsync(person);

// Tycho knows how to extract the ID
await db.ReadObjectAsync(person);
```

## Querying Objects

TychoDB offers rich querying capabilities:

### Basic Querying

```csharp
// Read all objects of a type
var allPeople = await db.ReadObjectsAsync();

// Read by ID
var person = await db.ReadObjectAsync("123");

// Check if an object exists
var exists = await db.ObjectExistsAsync("123");

// Count objects
var count = await db.CountObjectsAsync();
```

### Filtering

```csharp
// Create a filter for people older than 25
var filter = FilterBuilder
.Create()
.Filter(FilterType.GreaterThan, x => x.Age, 25);

// Apply the filter
var olderPeople = await db.ReadObjectsAsync(filter: filter);

// Chain multiple filters
var complexFilter = FilterBuilder
.Create()
.Filter(FilterType.GreaterThan, x => x.Age, 25)
.And()
.Filter(FilterType.Contains, x => x.Name, "Doe");

// Get a single object matching the filter
var johnDoe = await db.ReadObjectAsync(filter: complexFilter);

// Get the first object matching the filter
var firstPerson = await db.ReadFirstObjectAsync(filter: complexFilter);
```

### Sorting

```csharp
// Create a sort builder
var sort = SortBuilder
.Create()
.OrderBy(SortDirection.Ascending, x => x.Age)
.OrderBy(SortDirection.Descending, x => x.Name);

// Apply sorting with optional filtering
var sortedPeople = await db.ReadObjectsAsync(
filter: complexFilter,
sort: sort
);

// Limit the number of results
var topFivePeople = await db.ReadObjectsAsync(
filter: complexFilter,
sort: sort,
top: 5
);
```

### Nested Object Queries

```csharp
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string Country { get; set; }
}

public class Customer
{
public string Id { get; set; }
public string Name { get; set; }
public Address HomeAddress { get; set; }
}

// Query nested properties
var usCustomers = await db.ReadObjectsAsync(
filter: FilterBuilder
.Create()
.Filter(FilterType.Equals, x => x.HomeAddress.Country, "USA")
);

// Extract nested objects
var addresses = await db.ReadObjectsAsync(
x => x.HomeAddress
);

// Extract nested objects with keys
var addressesWithCustomerIds = await db.ReadObjectsWithKeysAsync(
x => x.HomeAddress
);

// Extract specific property from nested objects
var countries = await db.ReadObjectsAsync(
x => x.HomeAddress.Country
);
```

## Partitioning

Partitions allow you to organize your data logically:

```csharp
// Write objects to different partitions
await db.WriteObjectAsync(activePerson, x => x.Id, "active_users");
await db.WriteObjectAsync(inactivePerson, x => x.Id, "inactive_users");

// Read from a specific partition
var activeUsers = await db.ReadObjectsAsync(partition: "active_users");

// Count objects in a partition
var inactiveCount = await db.CountObjectsAsync(partition: "inactive_users");

// Delete all objects in a partition
var deletedCount = await db.DeleteObjectsAsync("inactive_users");
```

## BLOB Storage

For binary data:

```csharp
// Store a binary file
using var fileStream = File.OpenRead("document.pdf");
await db.WriteBlobAsync(fileStream, "doc_123", "documents");

// Check if a blob exists
var exists = await db.BlobExistsAsync("doc_123", "documents");

// Read a blob
using var blobStream = await db.ReadBlobAsync("doc_123", "documents");
// Use the stream...

// Delete a blob
await db.DeleteBlobAsync("doc_123", "documents");

// Delete all blobs in a partition
var result = await db.DeleteBlobsAsync("documents");
Console.WriteLine($"Deleted {result.Count} blobs");
```

## Indexing

Create indexes to improve query performance:

```csharp
// Create a simple index on a property
db.CreateIndex(x => x.Age, "age_index");

// Create an index asynchronously
await db.CreateIndexAsync(x => x.Name, "name_index");

// Create a composite index on multiple properties
db.CreateIndex(
new Expression>[]
{
x => x.Age,
x => x.Name
},
"age_name_index"
);

// Create an index on a nested property
db.CreateIndex(x => x.HomeAddress.Country, "country_index");
```

## Connection Management

TychoDB offers options for connection management:

```csharp
// Create a database with persistent connection (default)
var db = new Tycho(
dbPath: "./data",
jsonSerializer: serializer,
persistConnection: true
);

// Connect explicitly
db.Connect();

// Or connect asynchronously
await db.ConnectAsync();

// Disconnect when needed
db.Disconnect();

// Or disconnect asynchronously
await db.DisconnectAsync();
```

## Advanced Features

### Batch Operations

```csharp
// Write multiple objects at once
var people = GetManyPeople(); // Returns List
await db.WriteObjectsAsync(people, x => x.Id);

// Delete multiple objects with a filter
var deletedCount = await db.DeleteObjectsAsync(
filter: FilterBuilder
.Create()
.Filter(FilterType.LessThan, x => x.Age, 18)
);

// Delete all objects
await db.DeleteObjectsAsync();
```

### Database Maintenance

```csharp
// Optimize database performance and reduce size
db.Cleanup(shrinkMemory: true, vacuum: true);
```

## LINQ Support

TychoDB offers comprehensive LINQ support for more natural and familiar querying in C#. The LINQ interface lets you write type-safe queries with IntelliSense support and compile-time checking.

### Basic Querying with LINQ

```csharp
// Start a LINQ query for a specific type
var query = db.Query();

// Apply filters
var activeUsers = await db.Query()
.Where(p => p.IsActive)
.ToListAsync();

// Use multiple conditions
var seniorActiveUsers = await db.Query()
.Where(p => p.IsActive && p.Age > 65)
.ToListAsync();

// String operations
var gmailUsers = await db.Query()
.Where(p => p.Email.EndsWith("@gmail.com"))
.ToListAsync();
```

### Sorting and Paging

```csharp
// Order results
var orderedByAge = await db.Query()
.OrderBy(p => p.Age)
.ToListAsync();

// Order descending
var newestFirst = await db.Query()
.OrderByDescending(p => p.RegistrationDate)
.ToListAsync();

// Multiple ordering criteria
var sortedPeople = await db.Query()
.OrderBy(p => p.LastName)
.ThenBy(p => p.FirstName)
.ToListAsync();

// Limit results (pagination)
var topFive = await db.Query()
.OrderByDescending(p => p.Points)
.Take(5)
.ToListAsync();
```

### Single Result Operations

```csharp
// Get first matching result or default
var person = await db.Query()
.Where(p => p.Id == "abc123")
.FirstOrDefaultAsync();

// Get single matching result or default (throws if multiple matches)
var uniquePerson = await db.Query()
.Where(p => p.Email == "unique@example.com")
.SingleOrDefaultAsync();
```

### Aggregation Operations

```csharp
// Count results
int activeCount = await db.Query()
.Where(p => p.IsActive)
.CountAsync();

// Check existence
bool hasInactivePeople = await db.Query()
.Where(p => !p.IsActive)
.AnyAsync();
```

### Working with Partitions

```csharp
// Query within a specific partition
var europeUsers = await db.Query("europe")
.Where(p => p.Age > 18)
.ToListAsync();
```

### Complex Queries

```csharp
// Complex multi-condition queries
var result = await db.Query()
.Where(p => p.IsActive && p.Age > 25)
.Where(p => p.Email.EndsWith("@gmail.com") || p.Points >= 150)
.OrderByDescending(p => p.Points)
.Take(10)
.ToListAsync();
```

### Saving Collections with LINQ Extensions

```csharp
// Save a collection of objects
var people = new List
{
new Person { Id = "1", Name = "John Doe", Age = 30 },
new Person { Id = "2", Name = "Jane Smith", Age = 25 },
new Person { Id = "3", Name = "Bob Johnson", Age = 40 }
};

// Save all objects with a single call
await db.SaveAllAsync(people);

// Save to a specific partition
await db.SaveAllAsync(people, "active_users");
```

## Performance Considerations

- Use batch operations when dealing with multiple objects
- Create indexes for frequently queried properties
- Use the appropriate serializer for your needs (MessagePack for best performance)
- Consider partitioning for large datasets
- Use connection pooling for multi-threaded applications

## License

This project is licensed under the MIT License - see the LICENSE file for details.

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## Acknowledgments

- SQLite for providing the underlying database engine
- The .NET community for support and inspiration