Overview
Adding a new database provider involves:- Implementing the
IConnectionProviderinterface - Writing database-specific SQL queries
- Handling database-specific connection types
- Registering the provider
- Testing the implementation
Before implementing a new provider, check if someone is already working on it in the GitHub Issues.
The IConnectionProvider Interface
All database providers must implement this interface: Location:src/Queryly.Core/Connections/IConnectionProvider.cs
Interface Members
| Member | Description |
|---|---|
Type | Returns the DatabaseType enum value for this provider |
OpenConnectionAsync | Opens and returns a database connection |
TestConnectionAsync | Tests if a connection string is valid |
GetDatabasesAsync | Returns list of databases (or single database name) |
GetTablesAsync | Returns list of tables in the database |
GetColumnsAsync | Returns columns for a specific table |
Step-by-Step Implementation
public enum DatabaseType
{
SQLite,
SQLServer,
PostgreSQL,
MySQL,
Oracle // Add your database here
}
public static class DatabaseTypeExtensions
{
public static string TypeToString(this DatabaseType dbType)
{
return dbType switch
{
DatabaseType.SQLite => "SQLite",
DatabaseType.SQLServer => "SQLServer",
DatabaseType.MySQL => "MySQL",
DatabaseType.PostgreSQL => "PostgreSQL",
DatabaseType.Oracle => "Oracle", // Add mapping
_ => "Unknown"
};
}
}
MySql.DataNpgsqlMicrosoft.Data.SqlClientOracle.ManagedDataAccess.CoreMongoDB.Driverusing System.Data.Common;
using YourDatabase.Client; // Your database client namespace
namespace Queryly.Providers.YourDatabase;
public class YourDatabaseConnectionProvider : IConnectionProvider
{
// 1. Specify the database type
public DatabaseType Type => DatabaseType.YourDatabase;
// 2. Open a connection
public async Task<DbConnection> OpenConnectionAsync(string connectionString)
{
if (string.IsNullOrWhiteSpace(connectionString))
throw new ArgumentException(
"Connection string cannot be null or empty.",
nameof(connectionString)
);
try
{
var connection = new YourDatabaseConnection(connectionString);
await connection.OpenAsync();
return connection;
}
catch (YourDatabaseException ex)
{
throw new Exception(
$"Failed to open connection: {ex.Message}",
ex
);
}
}
// 3. Test connection
public async Task<bool> TestConnectionAsync(string connectionString)
{
try
{
using var connection = new YourDatabaseConnection(connectionString);
await connection.OpenAsync();
return true;
}
catch
{
return false;
}
}
// 4. Get databases
public List<string> GetDatabasesAsync(DbConnection connection)
{
if (connection is not YourDatabaseConnection)
throw new ArgumentException(
"Connection must be a YourDatabase connection.",
nameof(connection)
);
var sqlConn = (YourDatabaseConnection)connection;
var builder = new YourDatabaseConnectionStringBuilder(sqlConn.ConnectionString);
return new List<string> { builder.Database ?? "default" };
}
// 5. Get tables
public async Task<List<TableInfo>> GetTablesAsync(
DbConnection connection,
string database)
{
if (connection is not YourDatabaseConnection)
throw new ArgumentException(
"Connection must be a YourDatabase connection.",
nameof(connection)
);
try
{
var tables = new List<TableInfo>();
var sqlConn = (YourDatabaseConnection)connection;
var command = sqlConn.CreateCommand();
// Database-specific SQL to list tables
command.CommandText = "SELECT table_name FROM information_schema.tables";
using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
var tableName = reader.GetString(0);
// Optionally get row count
var countCommand = sqlConn.CreateCommand();
countCommand.CommandText = $"SELECT COUNT(*) FROM {tableName}";
var rowCount = Convert.ToInt64(await countCommand.ExecuteScalarAsync() ?? 0L);
tables.Add(new TableInfo
{
Name = tableName,
RowCount = rowCount
});
}
return tables;
}
catch (YourDatabaseException ex)
{
throw new Exception(
$"Failed to retrieve tables: {ex.Message}",
ex
);
}
}
// 6. Get columns
public async Task<List<ColumnInfo>> GetColumnsAsync(
DbConnection connection,
string database,
string table)
{
if (connection is not YourDatabaseConnection)
throw new ArgumentException(
"Connection must be a YourDatabase connection.",
nameof(connection)
);
if (string.IsNullOrWhiteSpace(table))
throw new ArgumentNullException(
nameof(table),
"Table name cannot be null."
);
try
{
var columns = new List<ColumnInfo>();
var sqlConn = (YourDatabaseConnection)connection;
var command = sqlConn.CreateCommand();
// Database-specific SQL to get column info
command.CommandText = @"
SELECT
column_name,
data_type,
is_nullable,
column_default
FROM information_schema.columns
WHERE table_name = @tableName
ORDER BY ordinal_position";
command.Parameters.AddWithValue("@tableName", table);
using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
var columnName = reader.GetString(0);
var dataType = reader.GetString(1);
var isNullable = reader.GetString(2) == "YES";
var defaultValue = reader.IsDBNull(3) ? null : reader.GetString(3);
columns.Add(new ColumnInfo
{
Name = columnName,
DataType = dataType,
IsNullable = isNullable,
DefaultValue = defaultValue
});
}
return columns;
}
catch (YourDatabaseException ex)
{
throw new Exception(
$"Failed to retrieve columns for table '{table}': {ex.Message}",
ex
);
}
}
}
IConnectionProvider provider = dbType switch
{
DatabaseType.SQLite => new SqliteConnectionProvider(),
DatabaseType.PostgreSQL => new PostgreSQLConnectionProvider(),
DatabaseType.MySQL => new MySQLConnectionProvider(),
DatabaseType.SQLServer => new SqlServerConnectionProvider(),
DatabaseType.YourDatabase => new YourDatabaseConnectionProvider(), // Add this
_ => throw new NotSupportedException($"Database type {dbType} not supported")
};
using Xunit;
using FluentAssertions;
using Queryly.Providers.YourDatabase;
namespace Queryly.Tests.Providers;
public class YourDatabaseConnectionProviderTests
{
[Fact]
public async Task OpenConnectionAsync_WithValidConnectionString_ReturnsOpenConnection()
{
// Arrange
var provider = new YourDatabaseConnectionProvider();
var connectionString = "your-test-connection-string";
// Act
var connection = await provider.OpenConnectionAsync(connectionString);
// Assert
connection.Should().NotBeNull();
connection.State.Should().Be(ConnectionState.Open);
// Cleanup
await connection.DisposeAsync();
}
[Fact]
public async Task GetTablesAsync_WithEmptyDatabase_ReturnsEmptyList()
{
// Arrange
var provider = new YourDatabaseConnectionProvider();
var connectionString = "your-test-connection-string";
using var connection = await provider.OpenConnectionAsync(connectionString);
// Act
var tables = await provider.GetTablesAsync(connection, "testdb");
// Assert
tables.Should().BeEmpty();
}
// Add more tests...
}
Reference Implementation: SQLite
Here’s the complete SQLite provider as a reference: File:src/Queryly.Providers/Sqlite/SqliteConnectionProvider.cs
Database-Specific SQL Queries
Each database has different SQL syntax for metadata queries:- SQLite
- PostgreSQL
- MySQL
- SQL Server
Data Models Reference
TableInfo
ColumnInfo
Testing Your Provider
Test your provider thoroughly:Manual Testing
Automated Testing
Common Challenges
Challenge 1: Connection String Parsing
Different databases have different connection string formats:Challenge 2: Metadata Queries
Some databases don’t support INFORMATION_SCHEMA:Challenge 3: Data Type Mapping
Map database-specific types to common types:Submitting Your Provider
Once your provider is complete:Follow the Contributing Guide to submit your PR.
Need Help?
If you’re stuck while implementing a provider:- Check existing provider implementations for reference
- Ask in GitHub Discussions
- Open a draft PR for early feedback
- Contact the maintainer: @Ayine-nongre