-
Notifications
You must be signed in to change notification settings - Fork 0
Home
A comprehensive guide to using the SQLite Toolkit for BHoM, covering everything from basic operations to advanced scenarios.
- Getting Started
- Understanding the Three-Tier Strategy
- Working with IRecord Objects
- Custom Property Mapping
- Filtering and Querying
- Schema Management
- Error Handling
- Performance Optimisation
- Best Practices
- Troubleshooting
- BHoM Framework installed
- .NET 6.0 or later
- SQLite database file or ability to create one
using BH.Adapter.SQLite;
using BH.oM.Adapter.Commands;
using BH.oM.SQLite.Examples;
using BH.oM.SQLite.Requests;
// Create adapter instance
var adapter = new SQLiteAdapter("MyProject.db");
// Your operations here...
// Close connection when finished
adapter.Execute(new Close());
// Create sample data
var readings = new List<SensorReading>
{
new SensorReading
{
SensorId = "SENSOR_001",
Temperature = 22.5,
Humidity = 45.0,
Timestamp = DateTime.Now,
IsValid = true,
StatusCode = 200
}
};
// Push to database (automatically creates table)
adapter.Push(readings);
// Pull all data back
var allReadings = adapter.Pull();
The SQLite Toolkit automatically selects the best mapping approach for your data using a sophisticated three-tier strategy:
When it's used: Objects implementing the IRecord
interface.
Advantages:
- Zero configuration required
- Automatic table creation
- Optimal performance
- Full type safety
Example:
public class TemperatureReading : BHoMObject, IRecord
{
[Description("Unique sensor identifier")]
public virtual string SensorId { get; set; } = "";
[Description("Temperature reading in degrees Celsius")]
public virtual double Temperature { get; set; } = 0.0;
[Description("Reading timestamp")]
public virtual DateTime Timestamp { get; set; } = DateTime.Now;
[Description("Data quality indicator")]
public virtual bool IsValid { get; set; } = true;
}
// Usage - completely automatic
var readings = new List<TemperatureReading> { /* your data */ };
adapter.Push(readings); // Table created automatically with optimal schema
When it's used: Complex BHoM objects requiring custom property mapping or exclusions.
Advantages:
- Full control over column naming
- Property exclusion capabilities
- Nested property mapping
- Conflict resolution
Example:
public class StructuralProject : BHoMObject
{
public override string Name { get; set; } = "";
public int ProjectId { get; set; }
public string Description { get; set; } = "";
public DateTime StartDate { get; set; }
public double Budget { get; set; }
public string InternalNotes { get; set; } = ""; // Will be excluded
}
// Custom mapping configuration
var config = new PushConfig
{
PropertyMappings = new Dictionary<string, string>
{
{ "Name", "ProjectName" }, // Rename column
{ "ProjectId", "ProjectNumber" }, // Rename column
{ "StartDate", "CommencementDate" } // Rename column
},
ExcludedProperties = new List<string>
{
"InternalNotes", // Exclude sensitive data
"CustomData" // Exclude complex objects
}
};
// Usage with custom mapping
adapter.Push(projects, config);
When it's used: Simple objects with only primitive properties that don't implement IRecord.
Advantages:
- Works with any simple object
- Automatic primitive property detection
- No configuration required
Example:
public class SimpleMetric : BHoMObject
{
public int MetricId { get; set; }
public string MetricName { get; set; } = "";
public double Value { get; set; }
public DateTime RecordedAt { get; set; }
}
// Automatically handled as primitive fallback
adapter.Push(metrics); // Only primitive properties are mapped
The most efficient way to work with the SQLite Toolkit is to implement the IRecord
interface:
public class MaterialProperties : BHoMObject, IRecord
{
[Description("Material name identifier")]
public virtual string MaterialName { get; set; } = "";
[Description("Density in kg/m³")]
public virtual double Density { get; set; } = 0.0;
[Description("Young's modulus in Pa")]
public virtual double YoungModulus { get; set; } = 0.0;
[Description("Poisson's ratio")]
public virtual double PoissonRatio { get; set; } = 0.0;
[Description("Material type classification")]
public virtual MaterialType Type { get; set; } = MaterialType.Unknown;
[Description("Recyclability indicator")]
public virtual bool IsRecyclable { get; set; } = false;
[Description("Cost per cubic metre")]
public virtual decimal CostPerCubicMeter { get; set; } = 0.0m;
}
public enum MaterialType
{
Unknown = 0,
Steel = 1,
Concrete = 2,
Wood = 3,
Aluminium = 4
}
- Automatic Schema Generation: Tables and columns created automatically
- Type Safety: Full compile-time type checking
- Performance: Optimised SQL generation and execution
- Maintenance: Changes to your objects automatically reflect in the database
public class OptimalSensorReading : BHoMObject, IRecord
{
// Use virtual properties for BHoM compatibility
[Description("Sensor device identifier")]
public virtual string SensorId { get; set; } = "";
// Include units in descriptions
[Description("Temperature reading in degrees Celsius")]
public virtual double Temperature { get; set; } = 0.0;
// Use descriptive property names
[Description("Data quality validation result")]
public virtual bool IsValid { get; set; } = true;
// Enums are fully supported
[Description("Sensor operational status")]
public virtual SensorStatus Status { get; set; } = SensorStatus.Active;
// DateTime properties work seamlessly
[Description("Measurement timestamp in UTC")]
public virtual DateTime Timestamp { get; set; } = DateTime.UtcNow;
}
var config = new PushConfig
{
PropertyMappings = new Dictionary<string, string>
{
{ "Name", "DisplayName" },
{ "Id", "Identifier" },
{ "Value", "MeasuredValue" }
}
};
public class ComplexProject : BHoMObject
{
public ProjectDetails Details { get; set; } = new ProjectDetails();
public ProjectStatistics Stats { get; set; } = new ProjectStatistics();
}
var config = new PushConfig
{
PropertyMappings = new Dictionary<string, string>
{
{ "Details.Name", "ProjectName" },
{ "Details.Manager", "ProjectManager" },
{ "Stats.ElementCount", "TotalElements" },
{ "Stats.TotalCost", "ProjectBudget" }
}
};
var config = new PushConfig
{
ExcludedProperties = new List<string>
{
"InternalId", // Internal system field
"TempData", // Temporary processing data
"ComplexCalculations", // Complex nested objects
"CachedResults" // Runtime cache data
}
};
public class EngineeringAnalysis : BHoMObject
{
public string AnalysisName { get; set; } = "";
public AnalysisParameters Parameters { get; set; } = new AnalysisParameters();
public List<AnalysisResult> Results { get; set; } = new List<AnalysisResult>();
public Dictionary<string, object> Metadata { get; set; } = new Dictionary<string, object>();
}
var advancedConfig = new PushConfig
{
PropertyMappings = new Dictionary<string, string>
{
// Flatten nested objects
{ "Parameters.LoadCase", "LoadCaseName" },
{ "Parameters.Safety", "SafetyFactor" },
{ "Parameters.Method", "AnalysisMethod" },
// Rename for database conventions
{ "AnalysisName", "analysis_name" },
{ "CreatedDate", "created_at" }
},
ExcludedProperties = new List<string>
{
"Results", // Store separately due to complexity
"Metadata", // Complex dictionary - handle separately
"TempFiles", // Temporary analysis files
"CacheData" // Runtime cache
}
};
Basic equality filtering for exact matches:
var exactMatch = new EqualityFilterRequest
{
TableName = "SensorReading",
ColumnFilters = new List<ColumnFilter>
{
new ColumnFilter
{
ColumnName = "SensorId",
Values = new List<object> { "TEMP_001" }
}
}
};
var results = adapter.Pull(exactMatch);
Multiple values (IN clause):
var multipleValues = new EqualityFilterRequest
{
TableName = "MaterialProperties",
ColumnFilters = new List<ColumnFilter>
{
new ColumnFilter
{
ColumnName = "Type",
Values = new List<object> { "Steel", "Concrete", "Aluminium" }
}
}
};
Multiple conditions with logical operators:
var complexFilter = new EqualityFilterRequest
{
TableName = "SensorReading",
ColumnFilters = new List<ColumnFilter>
{
new ColumnFilter
{
ColumnName = "IsValid",
Values = new List<object> { true }
},
new ColumnFilter
{
ColumnName = "StatusCode",
Values = new List<object> { 200, 201 }
}
},
Logic = LogicalOperator.And, // AND conditions together
MaxResults = 100 // Limit results
};
Numeric ranges:
var temperatureRange = new RangeFilterRequest
{
TableName = "SensorReading",
ColumnRanges = new Dictionary<string, GeneralDomain>
{
{ "Temperature", new GeneralDomain(20.0, 30.0) },
{ "Humidity", new GeneralDomain(40.0, 60.0) }
},
InclusiveBounds = true, // 20 <= Temperature <= 30
MaxResults = 500
};
Date ranges:
var dateRange = new RangeFilterRequest
{
TableName = "SensorReading",
ColumnRanges = new Dictionary<string, GeneralDomain>
{
{ "Timestamp", new GeneralDomain(
DateTime.Today.AddDays(-7), // Last 7 days
DateTime.Now
)}
}
};
Simple aggregation:
var averageTemp = new CustomSqlRequest
{
SqlQuery = "SELECT AVG(Temperature) as AverageTemperature FROM SensorReading WHERE IsValid = @valid",
Parameters = new Dictionary<string, object>
{
{ "@valid", true }
}
};
Complex analytical queries:
var complexAnalysis = new CustomSqlRequest
{
SqlQuery = @"
SELECT
SensorId,
DATE(Timestamp) as ReadingDate,
AVG(Temperature) as AvgTemp,
MIN(Temperature) as MinTemp,
MAX(Temperature) as MaxTemp,
COUNT(*) as ReadingCount,
STDDEV(Temperature) as TempVariance
FROM SensorReading
WHERE
Timestamp BETWEEN @startDate AND @endDate
AND IsValid = @valid
AND StatusCode = @statusCode
GROUP BY SensorId, DATE(Timestamp)
HAVING COUNT(*) >= @minReadings
ORDER BY ReadingDate DESC, AvgTemp DESC
LIMIT @maxResults",
Parameters = new Dictionary<string, object>
{
{ "@startDate", DateTime.Today.AddDays(-30) },
{ "@endDate", DateTime.Now },
{ "@valid", true },
{ "@statusCode", 200 },
{ "@minReadings", 10 },
{ "@maxResults", 100 }
}
};
Join queries across tables:
var joinQuery = new CustomSqlRequest
{
SqlQuery = @"
SELECT
s.SensorId,
s.Temperature,
s.Timestamp,
m.MaterialName,
m.Density
FROM SensorReading s
INNER JOIN MaterialProperties m ON s.MaterialId = m.Id
WHERE
s.Temperature > @minTemp
AND m.Type = @materialType
ORDER BY s.Timestamp DESC",
Parameters = new Dictionary<string, object>
{
{ "@minTemp", 25.0 },
{ "@materialType", "Steel" }
}
};
Get information about all tables:
var allTables = new SchemaRequest
{
TableNames = new List<string>(), // Empty = all tables
IncludeColumns = true,
IncludeIndexes = true,
IncludeForeignKeys = true
};
var schema = adapter.Pull(allTables);
Get specific table information:
var specificTable = new SchemaRequest
{
TableNames = new List<string> { "SensorReading", "MaterialProperties" },
IncludeColumns = true,
IncludeIndexes = false,
IncludeForeignKeys = false
};
Query table with advanced options:
var tableQuery = new TableRequest
{
Name = "SensorReading",
Columns = new List<string>
{
"SensorId",
"Temperature",
"Humidity",
"Timestamp"
},
WhereConditions = new List<string>
{
"Temperature > 20",
"IsValid = 1",
"StatusCode = 200"
},
OrderBy = new List<string> { "Timestamp DESC", "Temperature ASC" },
Limit = 100,
Offset = 0,
Distinct = false
};
var results = adapter.Pull(tableQuery);
Pagination example:
// Get first page
var page1 = new TableRequest
{
Name = "SensorReading",
OrderBy = new List<string> { "Timestamp DESC" },
Limit = 50,
Offset = 0
};
// Get second page
var page2 = new TableRequest
{
Name = "SensorReading",
OrderBy = new List<string> { "Timestamp DESC" },
Limit = 50,
Offset = 50
};
try
{
adapter.Push(sensorData);
}
catch (Exception ex)
{
Console.WriteLine($"Error pushing data: {ex.Message}");
// Handle error appropriately
}
The toolkit provides extensive validation and will use BHoM's error recording system:
// Check for errors after operations
var queryResult = adapter.Pull(customSqlRequest);
var result = queryResult.FirstOrDefault() as QueryResult;
if (result != null && !result.IsSuccess)
{
Console.WriteLine($"Query failed: {result.ErrorMessage}");
// Handle failed query
}
- Invalid SQL Syntax:
var invalidQuery = new CustomSqlRequest
{
SqlQuery = "SELCT * FROM NonExistentTable", // Typo in SELECT
Parameters = new Dictionary<string, object>()
};
// Will return QueryResult with IsSuccess = false and descriptive ErrorMessage
- Missing Parameters:
var missingParamQuery = new CustomSqlRequest
{
SqlQuery = "SELECT * FROM SensorReading WHERE Temperature > @minTemp",
Parameters = new Dictionary<string, object>() // Missing @minTemp parameter
};
- Type Mismatches:
var typeMismatch = new CustomSqlRequest
{
SqlQuery = "SELECT * FROM SensorReading WHERE Temperature > @minTemp",
Parameters = new Dictionary<string, object>
{
{ "@minTemp", "not_a_number" } // String instead of number
}
};
Process large datasets efficiently:
// Process large datasets in batches
var largeMeasurementSet = GenerateLargeMeasurementSet(10000);
// Split into batches of 1000
var batches = largeMeasurementSet
.Select((x, i) => new { Index = i, Value = x })
.GroupBy(x => x.Index / 1000)
.Select(x => x.Select(v => v.Value).ToList())
.ToList();
foreach (var batch in batches)
{
adapter.Push(batch);
// Optional: progress reporting
Console.WriteLine($"Processed batch {batches.IndexOf(batch) + 1} of {batches.Count}");
}
// Efficient connection usage
using (var adapter = new SQLiteAdapter("LargeDataset.db"))
{
adapter.Execute(new Open() { FileName = "LargeDataset.db" });
// Perform multiple operations with same connection
adapter.Push(batch1);
adapter.Push(batch2);
adapter.Push(batch3);
var results = adapter.Pull(complexQuery);
// Connection automatically closed
}
Use indexed columns for better performance:
// Good: Filter on likely indexed columns
var optimisedQuery = new CustomSqlRequest
{
SqlQuery = @"
SELECT * FROM SensorReading
WHERE SensorId = @sensorId -- Likely indexed
AND Timestamp >= @startDate -- Likely indexed
ORDER BY Timestamp DESC -- Use index for sorting
LIMIT @maxResults",
Parameters = new Dictionary<string, object>
{
{ "@sensorId", "TEMP_001" },
{ "@startDate", DateTime.Today },
{ "@maxResults", 1000 }
}
};
- Always Use Parameterised Queries:
// Good - parameterised
var safeQuery = new CustomSqlRequest
{
SqlQuery = "SELECT * FROM SensorReading WHERE SensorId = @sensorId",
Parameters = new Dictionary<string, object> { { "@sensorId", userInput } }
};
// NEVER - SQL injection risk
var unsafeQuery = new CustomSqlRequest
{
SqlQuery = $"SELECT * FROM SensorReading WHERE SensorId = '{userInput}'"
};
- Validate Input Data:
public bool ValidateSensorReading(SensorReading reading)
{
if (string.IsNullOrWhiteSpace(reading.SensorId))
return false;
if (reading.Temperature < -273.15 || reading.Temperature > 1000)
return false;
if (reading.Humidity < 0 || reading.Humidity > 100)
return false;
return true;
}
- Use Appropriate Data Types:
public class WellDesignedReading : BHoMObject, IRecord
{
public virtual string SensorId { get; set; } = ""; // String for IDs
public virtual double Temperature { get; set; } = 0.0; // Double for measurements
public virtual DateTime Timestamp { get; set; } = DateTime.UtcNow; // DateTime for timestamps
public virtual bool IsValid { get; set; } = true; // Boolean for flags
public virtual int StatusCode { get; set; } = 200; // Integer for codes
public virtual decimal Cost { get; set; } = 0.0m; // Decimal for money
}
- Design for Querying:
public class QueryOptimisedReading : BHoMObject, IRecord
{
// Primary identifier - will be queried frequently
public virtual string SensorId { get; set; } = "";
// Timestamp - will be used for time-based queries
public virtual DateTime Timestamp { get; set; } = DateTime.UtcNow;
// Status flags - will be used for filtering
public virtual bool IsValid { get; set; } = true;
public virtual ReadingStatus Status { get; set; } = ReadingStatus.Normal;
// Measurement data
public virtual double Temperature { get; set; } = 0.0;
public virtual double Humidity { get; set; } = 0.0;
}
Cause: Attempting to query a table that hasn't been created yet. Solution: Ensure you've pushed data of that type first, or the table exists.
// Ensure table exists by pushing data first
adapter.Push(new List<SensorReading> { new SensorReading() });
// Now queries will work
var results = adapter.Pull(new EqualityFilterRequest { TableName = "SensorReading" });
Cause: Property name doesn't match expected column name. Solution: Check your property mappings or use correct property names.
// Check what columns actually exist
var schema = adapter.Pull(new SchemaRequest
{
TableNames = new List<string> { "SensorReading" },
IncludeColumns = true
});
Cause: Using string concatenation instead of parameters. Solution: Always use parameterised queries.
// Wrong
var query = $"SELECT * FROM SensorReading WHERE SensorId = '{sensorId}'";
// Correct
var request = new CustomSqlRequest
{
SqlQuery = "SELECT * FROM SensorReading WHERE SensorId = @sensorId",
Parameters = new Dictionary<string, object> { { "@sensorId", sensorId } }
};
Cause: Not using batch operations or appropriate filtering. Solution: Implement batch processing and use indexed columns for filtering.
// Use pagination for large result sets
var pagedResults = new TableRequest
{
Name = "SensorReading",
OrderBy = new List<string> { "Timestamp DESC" },
Limit = 1000, // Reasonable page size
Offset = 0
};
- Enable Detailed Error Messages:
var result = adapter.Pull(customQuery).FirstOrDefault() as QueryResult;
if (result != null && !result.IsSuccess)
{
Console.WriteLine($"Query failed: {result.ErrorMessage}");
Console.WriteLine($"SQL: {customQuery.SqlQuery}");
Console.WriteLine($"Parameters: {string.Join(", ", customQuery.Parameters)}");
}
- Validate Your Objects:
// Check if your object implements IRecord
var isIRecord = typeof(IRecord).IsAssignableFrom(typeof(YourObjectType));
Console.WriteLine($"Implements IRecord: {isIRecord}");
// Check property types
foreach (var prop in typeof(YourObjectType).GetProperties())
{
Console.WriteLine($"{prop.Name}: {prop.PropertyType}");
}
- Test with Simple Cases First:
// Start with minimal test case
var simpleTest = new List<SensorReading>
{
new SensorReading { SensorId = "TEST", Temperature = 20.0 }
};
adapter.Push(simpleTest);
var retrieved = adapter.Pull();
Console.WriteLine($"Pushed 1, retrieved {retrieved.Count()}");
If you encounter issues not covered in this guide:
-
Check the test suite: The comprehensive test suite in
SQLite_Toolkit.Tests/
provides working examples of all functionality - Review error messages: The toolkit provides detailed error messages through BHoM's error recording system
- Consult BHoM documentation: https://bhom.xyz/documentation/
- Open an issue: Report bugs or request features on the project's GitHub repository
This user guide covers the essential aspects of working with the SQLite Toolkit. The toolkit is designed to be intuitive and follows BHoM conventions, making it easy to integrate into your existing BHoM workflows.