SchemaExtendedReader
An experiment in using ADO 2.0 DbProviderFactories for SQLServer/Oracle/MySql/SQLite/Firebird.
Now a CodePlex project - see there for latest code.
- SchemaReader - use DbProviderFactories to read database metadata.
- SchemaExtendedReader - provides more SqlServer/Oracle/MySql schema information not available in GetSchema.
- DatabaseReader - calls SchemaReader to build a database model
- SchemaConverter - converts DbProviderFactory datatables into a database model
- CreateSql - build (simple) select/insert/update sql using SchemaReader above
More information from SqlServer, Oracle and MySql (5.1+). The constraints from GetSchema don't work well, so here we do them better. Also you can make agnostic calls to Oracle packages, SqlServer/MySql Identity and check constraints for both.
using System;
using System.Data;
using System.Data.Common;
using System.Globalization;
namespace Library.Data
{
/// <summary>
/// Extended schema information beyond that included in GetSchema.
/// </summary>
public class SchemaExtendedReader : SchemaReader
{
/// <summary>
/// Constructor with connectionString and ProviderName
/// </summary>
/// <param name="connectionString">Eg "Data Source=localhost;Integrated Security=SSPI;Initial Catalog=Northwind;"</param>
/// <param name="providerName">ProviderInvariantName for the provider (eg System.Data.SqlClient or System.Data.OracleClient)</param>
public SchemaExtendedReader(string connectionString, string providerName)
: base(connectionString, providerName)
{
}
/// <summary>
/// Gets a value indicating whether this provider is SQL server.
/// </summary>
/// <value>
/// <c>true</c> if this instance is SQL server; otherwise, <c>false</c>.
/// </value>
internal bool IsSqlServer
{
get
{
//SqlClient
return (_providerName.Equals("System.Data.SqlClient", StringComparison.OrdinalIgnoreCase));
}
}
/// <summary>
/// Get all data for a specified table name.
/// </summary>
/// <param name="tableName">Name of the table. Oracle names can be case sensitive.</param>
/// <returns>A dataset containing the tables: Columns, Primary_Keys, Foreign_Keys, Unique_Keys (only filled for Oracle), Indexes, IndexColumns, Triggers</returns>
public override DataSet Table(string tableName)
{
if (!IsSqlServer && !IsOracle && !IsMySql)
return base.Table(tableName);
//more information from sqlserver, oracle and mysql
DataSet ds = new DataSet();
using (DbConnection conn = _factory.CreateConnection())
{
conn.ConnectionString = _connectionString;
conn.Open();
//uses friend access to schemaReader
LoadTable(tableName, ds, conn);
if (ds.Tables.Count == 0) return null; //no data found
DataTable pks = FindKeys(tableName, GetPrimaryKeyType(), conn);
pks.TableName = "PRIMARY_KEYS";
ds.Tables.Add(pks);
DataTable fks = FindKeys(tableName, GetForeignKeyType(), conn);
fks.TableName = "FOREIGN_KEYS";
ds.Tables.Add(fks);
ds.Tables.Add(ForeignKeyColumns(tableName));
DataTable uks = FindKeys(tableName, GetUniqueKeyType(), conn);
uks.TableName = "UNIQUE_KEYS";
ds.Tables.Add(uks);
DataTable cks = FindChecks(tableName, conn);
ds.Tables.Add(cks);
ds.Tables.Add(IdentityColumns(tableName, conn));
}
return ds;
}
protected override DataTable Columns(string tableName, DbConnection connection)
{
if (!IsOracle)
return base.Columns(tableName, connection);
//for Oracle, we do our own thing
DataTable dt = new DataTable("Columns");
using (DbDataAdapter da = _factory.CreateDataAdapter())
{
//this is almost exactly the same sql as the System.Data.OracleClient uses, plus data_default. We use Char_Length (chars) rather than Data_Length (bytes)
string sqlCommand = @"SELECT OWNER,
TABLE_NAME,
COLUMN_NAME,
COLUMN_ID AS ID,
DATA_TYPE AS DataType,
CHAR_LENGTH AS LENGTH,
DATA_PRECISION AS PRECISION,
DATA_SCALE AS Scale,
NULLABLE AS Nullable,
DATA_DEFAULT AS Column_default
FROM ALL_TAB_COLUMNS
WHERE
TABLE_NAME NOT LIKE 'BIN$%'
AND (OWNER = :OWNER
OR :OWNER IS NULL)
AND (TABLE_NAME = :TABLENAME
OR :TABLENAME IS NULL)
ORDER BY OWNER,
TABLE_NAME,
ID";
using (DbCommand cmd = connection.CreateCommand())
{
cmd.CommandText = sqlCommand;
EnsureOracleBindByName(cmd);
cmd.Parameters.Add(
AddDbParameter("OWNER", Owner));
cmd.Parameters.Add(
AddDbParameter("TABLENAME", tableName));
da.SelectCommand = cmd;
da.Fill(dt);
}
}
return dt;
}
public DataTable IdentityColumns(string tableName)
{
using (DbConnection conn = _factory.CreateConnection())
{
conn.ConnectionString = _connectionString;
conn.Open();
return IdentityColumns(tableName, conn);
}
}
private DataTable IdentityColumns(string tableName, DbConnection conn)
{
DataTable dt = new DataTable("IdentityColumns");
dt.Locale = CultureInfo.InvariantCulture;
if (!IsSqlServer && !IsMySql) return dt; //Oracle has sequences instead
//create a dataadaptor and fill it
using (DbDataAdapter da = _factory.CreateDataAdapter())
{
string sqlCommand;
if (IsSqlServer) sqlCommand = @"SELECT
SchemaOwner = s.name,
TableName = o.name,
ColumnName = c.name
FROM sys.identity_columns c
INNER JOIN sys.all_objects o ON c.object_id = o.object_id
INNER JOIN sys.schemas s ON s.schema_id = o.schema_id
WHERE
(o.name = @tableName OR @tableName IS NULL) AND
(s.name = @schemaOwner OR @schemaOwner IS NULL) AND
o.type= 'U'
ORDER BY o.name, c.name";
else
//MySql version using information_schema
sqlCommand = @"SELECT
TABLE_SCHEMA AS 'SchemaOwner',
TABLE_NAME AS 'TableName',
COLUMN_NAME AS 'ColumnName'
FROM information_schema.columns
WHERE EXTRA = 'auto_increment' AND
(TABLE_NAME = @tableName OR @tableName IS NULL) AND
(TABLE_SCHEMA = @schemaOwner OR @schemaOwner IS NULL)";
da.SelectCommand = conn.CreateCommand();
da.SelectCommand.CommandText = sqlCommand;
da.SelectCommand.Parameters.Add(
AddDbParameter("tableName", tableName));
da.SelectCommand.Parameters.Add(
AddDbParameter("schemaOwner", Owner));
da.Fill(dt);
return dt;
}
}
public DataTable Triggers(string tableName)
{
using (DbConnection conn = _factory.CreateConnection())
{
conn.ConnectionString = _connectionString;
conn.Open();
if (!IsOracle && !IsSqlServer && !IsMySql)
{
return GenericCollection("Triggers", conn, tableName);
}
return Triggers(tableName, conn);
}
}
private DataTable Triggers(string tableName, DbConnection conn)
{
const string collectionName = "Triggers";
DataTable dt = new DataTable(collectionName);
dt.Locale = CultureInfo.InvariantCulture;
//create a dataadaptor and fill it
using (DbDataAdapter da = _factory.CreateDataAdapter())
{
string sqlCommand;
if (IsOracle)
{
sqlCommand = @"SELECT OWNER,
TRIGGER_NAME,
TABLE_NAME,
TRIGGER_BODY,
TRIGGERING_EVENT
FROM ALL_TRIGGERS
WHERE STATUS = 'ENABLED' AND
(TABLE_NAME = :tableName OR :tableName IS NULL) AND
(OWNER = :schemaOwner OR :schemaOwner IS NULL) AND
TRIGGER_NAME NOT IN ( SELECT object_name FROM USER_RECYCLEBIN )";
}
else if (IsMySql)
{
sqlCommand = @"SELECT
TRIGGER_SCHEMA AS 'OWNER',
TRIGGER_NAME,
EVENT_OBJECT_TABLE AS 'TABLE_NAME',
ACTION_STATEMENT AS 'TRIGGER_BODY',
EVENT_MANIPULATION AS 'TRIGGERING_EVENT'
FROM information_schema.Triggers
WHERE
(EVENT_OBJECT_TABLE = @tableName OR @tableName IS NULL) AND
(TRIGGER_SCHEMA = @schemaOwner OR @schemaOwner IS NULL)";
}
else
{
sqlCommand = @"SELECT SCHEMA_NAME(o1.uid) AS 'OWNER',
o1.NAME AS 'TRIGGER_NAME',
o2.NAME AS 'TABLE_NAME',
NULL AS 'TRIGGER_BODY',
NULL AS 'TRIGGERING_EVENT'
FROM sysobjects o1
INNER JOIN sysobjects o2 ON o1.parent_obj = o2.id
WHERE o1.XTYPE = 'TR' AND
(o1.NAME = @tableName OR @tableName IS NULL) AND
(SCHEMA_NAME(o1.uid) = @schemaOwner OR @schemaOwner IS NULL)";
}
da.SelectCommand = conn.CreateCommand();
EnsureOracleBindByName(da.SelectCommand);
da.SelectCommand.CommandText = sqlCommand;
da.SelectCommand.Parameters.Add(
AddDbParameter("tableName", tableName));
da.SelectCommand.Parameters.Add(
AddDbParameter("schemaOwner", Owner));
da.Fill(dt);
return dt;
}
}
/// <summary>
/// Find the functions. In SqlServer, they are mixed in with sprocs.
/// </summary>
/// <returns></returns>
/// <exception cref="System.Data.Common.DbException">Thrown when there is no security access to read DDL</exception>
public override DataTable Functions()
{
DataTable dt = new DataTable("Functions");
if (!IsOracle) return dt; //in sql server, functions are in the sprocs collection.
using (DbConnection conn = _factory.CreateConnection())
{
conn.ConnectionString = _connectionString;
conn.Open();
//create a dataadaptor and fill it
using (DbDataAdapter da = _factory.CreateDataAdapter())
{
string sqlCommand = @"SELECT OWNER,
OBJECT_NAME,
DBMS_METADATA.GET_DDL('FUNCTION', OBJECT_NAME, :OWNER) AS ""SQL""
FROM ALL_OBJECTS
WHERE (OWNER = :OWNER
OR :OWNER IS NULL)
AND OBJECT_TYPE = 'FUNCTION'";
da.SelectCommand = conn.CreateCommand();
da.SelectCommand.CommandText = sqlCommand;
EnsureOracleBindByName(da.SelectCommand);
da.SelectCommand.CommandText = sqlCommand;
da.SelectCommand.Parameters.Add(
AddDbParameter("OWNER", Owner));
da.Fill(dt);
return dt;
}
}
}
public DataTable ProcedureSource(string name)
{
DataTable dt = new DataTable("ProcedureSource");
DbProviderFactory factory = DbProviderFactories.GetFactory(_providerName);
using (DbConnection conn = factory.CreateConnection())
{
conn.ConnectionString = _connectionString;
conn.Open();
//create a dataadaptor and fill it
using (DbDataAdapter da = factory.CreateDataAdapter())
{
string sqlCommand = null;
if (IsSqlServer)
{
//NB: sql_modules in SQLServer 2005+.
//sqlServer2000 InformationSchema cuts the source after 4k, so use sq_help
//http://msdn.microsoft.com/en-us/library/ms178618.aspx type is sproc, function or CLR procedure
sqlCommand = @"SELECT
OBJECT_SCHEMA_NAME(o.object_id) AS ""OWNER"",
OBJECT_NAME(sm.object_id) AS ""NAME"",
o.type AS ""TYPE"",
sm.definition As ""TEXT""
FROM sys.sql_modules AS sm
JOIN sys.objects AS o
ON sm.object_id = o.object_id
WHERE (o.type = N'P' OR o.type = N'FN' OR o.type='PC')
AND (OBJECT_SCHEMA_NAME(o.object_id) = @schemaOwner OR @schemaOwner IS NULL)
AND (OBJECT_NAME(sm.object_id) = @name OR @name IS NULL)
ORDER BY o.type;";
}
else if (IsOracle)
{
//if you don't have security to view source, you get no rows back
sqlCommand = @"SELECT
OWNER,
NAME,
TYPE,
LINE,
TEXT
FROM ALL_SOURCE
WHERE
OWNER = :schemaOwner AND
TYPE IN ('PROCEDURE', 'FUNCTION', 'PACKAGE', 'PACKAGE BODY') AND
(NAME = :name OR :name IS NULL)
ORDER BY OWNER, NAME, TYPE, LINE";
}
if (sqlCommand == null) return dt;
da.SelectCommand = conn.CreateCommand();
da.SelectCommand.CommandText = sqlCommand;
EnsureOracleBindByName(da.SelectCommand);
da.SelectCommand.Parameters.Add(
AddDbParameter("schemaOwner", Owner));
da.SelectCommand.Parameters.Add(
AddDbParameter("name", name));
da.Fill(dt);
return dt;
}
}
}
private DbParameter AddDbParameter(string parameterName, object value)
{
DbParameter parameter = _factory.CreateParameter();
parameter.ParameterName = parameterName;
//C# null should be DBNull
parameter.Value = value ?? DBNull.Value;
return parameter;
}
private static void EnsureOracleBindByName(DbCommand cmd)
{
//Oracle.DataAccess.Client only binds first parameter match unless BindByName=true
//so we violate LiskovSP (in reflection to avoid dependency on ODP)
if (cmd.GetType().GetProperty("BindByName") != null)
{
cmd.GetType().GetProperty("BindByName").SetValue(cmd, true, null);
}
}
#region Constraints
/// <summary>
/// The PK columns for a specific table (if tableName is null or empty, all constraints are returned)
/// </summary>
/// <param name="tableName">Name of the table. Oracle names can be case sensitive.</param>
/// <returns>DataTable with constraint_name, table_name, column_name, ordinal_position</returns>
public override DataTable PrimaryKeys(string tableName)
{
if (!IsMySql && !IsOracle && !IsSqlServer)
return base.PrimaryKeys(tableName);
return FindKeys(tableName, GetPrimaryKeyType());
}
/// <summary>
/// The Foreign Key columns for a specific table (if tableName is null or empty, all constraints are returned)
/// </summary>
/// <param name="tableName">Name of the table. Oracle names can be case sensitive.</param>
public override DataTable ForeignKeys(string tableName)
{
if (!IsMySql && !IsOracle && !IsSqlServer)
return base.ForeignKeys(tableName);
return FindKeys(tableName, GetForeignKeyType());
}
public override DataTable ForeignKeyColumns(string tableName)
{
//doesn't exist in SqlServer- but we've overridden anyway
if (IsSqlServer || IsOracle || IsMySql) return new DataTable("ForeignKeyColumns");
return base.ForeignKeyColumns(tableName);
}
/// <summary>
/// The Unique Key columns for a specific table (if tableName is null or empty, all constraints are returned). This is Oracle only and returns an empty datatable for SqlServer.
/// </summary>
public DataTable UniqueKeys(string tableName)
{
return FindKeys(tableName, GetUniqueKeyType());
}
/// <summary>
/// The check constraints for a specific table (if tableName is null or empty, all check constraints are returned)
/// </summary>
public DataTable CheckConstraints(string tableName)
{
return FindChecks(tableName);
}
#region Constraint private methods
/// <summary>
/// Finds the primary/foreign/unique keys constraints
/// </summary>
/// <param name="tableName">Name of the table. Oracle names can be case sensitive.</param>
/// <param name="constraintType">Type of the constraint.</param>
private DataTable FindKeys(string tableName, string constraintType)
{
if (!IsMySql && !IsOracle && !IsSqlServer) return new DataTable();
//open a connection
using (DbConnection conn = _factory.CreateConnection())
{
conn.ConnectionString = _connectionString;
return FindKeys(tableName, constraintType, conn);
}
}
private DataTable FindKeys(string tableName, string constraintType, DbConnection conn)
{
DataTable dt = new DataTable(constraintType);
if (constraintType == "U" && !IsOracle)
return dt; //only Oracle has this concept
string sqlCommand = GetKeySql();
if (String.IsNullOrEmpty(sqlCommand)) return dt;
//create a dataadaptor and fill it
using (DbDataAdapter da = _factory.CreateDataAdapter())
{
da.SelectCommand = conn.CreateCommand();
EnsureOracleBindByName(da.SelectCommand);
da.SelectCommand.CommandText = sqlCommand;
da.SelectCommand.Parameters.Add(
AddDbParameter("tableName", tableName));
da.SelectCommand.Parameters.Add(
AddDbParameter("schemaOwner", Owner));
DbParameter type = _factory.CreateParameter();
type.ParameterName = "constraint_type";
type.Value = constraintType;
da.SelectCommand.Parameters.Add(type);
da.Fill(dt);
return dt;
}
}
private string GetPrimaryKeyType()
{
return IsOracle ? "P" : "PRIMARY KEY";
}
private string GetForeignKeyType()
{
return IsOracle ? "R" : "FOREIGN KEY";
}
private static string GetUniqueKeyType()
{
return "U";
}
/// <summary>
/// Gets the key SQL. GetSchema doesn't work :(
/// </summary>
private string GetKeySql()
{
string sqlCommand = null;
if (IsOracle)//Oracle doesn't have INFORMATION_SCHEMA
{
sqlCommand = @"SELECT cols.constraint_name,
cols.table_name,
cols.column_name,
cols.position AS ordinal_position,
cons.r_constraint_name AS unique_constraint_name,
cons2.table_name AS fk_table,
cons.delete_rule
FROM all_constraints cons
INNER JOIN all_cons_columns cols
ON cons.constraint_name = cols.constraint_name
AND cons.owner = cols.owner
LEFT OUTER JOIN all_constraints cons2
ON cons.r_constraint_name = cons2.constraint_name
AND cons.owner = cons2.owner
WHERE
(cols.table_name = :tableName OR :tableName IS NULL) AND
(cols.owner = :schemaOwner OR :schemaOwner IS NULL) AND
cons.constraint_type = :constraint_type
ORDER BY cols.table_name, cols.position";
}
else //if (IsSqlServer || IsMySql) //use SQL92 INFORMATION_SCHEMA
{
sqlCommand = @"SELECT cons.constraint_name,
cons.table_name,
column_name,
ordinal_position,
refs.unique_constraint_name,
cons2.table_name AS fk_table,
NULL AS delete_rule
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS cons
INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS keycolumns
ON (cons.constraint_catalog = keycolumns.constraint_catalog
OR cons.constraint_catalog IS NULL) AND
cons.constraint_schema = keycolumns.constraint_schema AND
cons.constraint_name = keycolumns.constraint_name
LEFT OUTER JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS refs
ON (cons.constraint_catalog = refs.constraint_catalog
OR cons.constraint_catalog IS NULL) AND
cons.constraint_schema = refs.constraint_schema AND
cons.constraint_name = refs.constraint_name
LEFT OUTER JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS cons2
ON (cons2.constraint_catalog = refs.constraint_catalog
OR cons2.constraint_catalog IS NULL) AND
cons2.constraint_schema = refs.constraint_schema AND
cons2.constraint_name = refs.unique_constraint_name
WHERE
(cons.table_name = @tableName OR @tableName IS NULL) AND
(cons.constraint_schema = @schemaOwner OR @schemaOwner IS NULL) AND
cons.constraint_type = @constraint_type";
}
return sqlCommand;
}
private string GetCheckSql()
{
string sqlCommand = null;
if (IsOracle)//Oracle doesn't have INFORMATION_SCHEMA
{
//all_constraints includes NULL constraints. They have generated names- so we exclude them.
sqlCommand = @"SELECT
cons.constraint_name,
cons.table_name,
cons.search_condition AS Expression
FROM all_constraints cons
WHERE
(cons.table_name = :tableName OR :tableName IS NULL) AND
(cons.owner = :schemaOwner OR :schemaOwner IS NULL) AND
cons.constraint_type = 'C' AND
cons.generated <> 'GENERATED NAME'
ORDER BY cons.table_name, cons.constraint_name";
}
else if (IsSqlServer) //use SQL92 INFORMATION_SCHEMA
{
//information_schema.check_constraints doesn't have table, so we join to table constraints
sqlCommand = @"SELECT
cons.constraint_name,
cons.table_name,
cons2.check_clause AS Expression
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS cons
INNER JOIN INFORMATION_SCHEMA.CHECK_CONSTRAINTS AS cons2
ON cons2.constraint_catalog = cons.constraint_catalog AND
cons2.constraint_schema = cons.constraint_schema AND
cons2.constraint_name = cons.constraint_name
WHERE
(cons.table_name = @tableName OR @tableName IS NULL) AND
(cons.constraint_catalog = @schemaOwner OR @schemaOwner IS NULL) AND
cons.constraint_type = 'CHECK'
ORDER BY cons.table_name, cons.constraint_name";
}
return sqlCommand;
}
private DataTable FindChecks(string tableName)
{
//open a connection
using (DbConnection conn = _factory.CreateConnection())
{
conn.ConnectionString = _connectionString;
return FindChecks(tableName, conn);
}
}
private DataTable FindChecks(string tableName, DbConnection conn)
{
DataTable dt = new DataTable("CHECKS");
string sqlCommand = GetCheckSql();
if (String.IsNullOrEmpty(sqlCommand)) return dt;
using (DbDataAdapter da = _factory.CreateDataAdapter())
{
da.SelectCommand = conn.CreateCommand();
EnsureOracleBindByName(da.SelectCommand);
da.SelectCommand.CommandText = sqlCommand;
da.SelectCommand.Parameters.Add(
AddDbParameter("tableName", tableName));
da.SelectCommand.Parameters.Add(
AddDbParameter("schemaOwner", Owner));
da.Fill(dt);
return dt;
}
}
#endregion
#endregion
}
}