diff --git a/.gitignore b/.gitignore index 917391e..e53e0f8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,10 @@ -**/bin/ -**/obj +**\bin +**\obj *.suo *.swp +*.csproj.user +*.nupkg +*.VisualState.xml +TestResult.xml +**\packages +*.zip diff --git a/.nuget/NuGet.Config b/.nuget/NuGet.Config new file mode 100644 index 0000000..6a318ad --- /dev/null +++ b/.nuget/NuGet.Config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.nuget/NuGet.exe b/.nuget/NuGet.exe new file mode 100644 index 0000000..b5c8886 Binary files /dev/null and b/.nuget/NuGet.exe differ diff --git a/.nuget/NuGet.targets b/.nuget/NuGet.targets new file mode 100644 index 0000000..e470f19 --- /dev/null +++ b/.nuget/NuGet.targets @@ -0,0 +1,71 @@ + + + + $(MSBuildProjectDirectory)\..\ + + + $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) + $([System.IO.Path]::Combine($(ProjectDir), "packages.config")) + $([System.IO.Path]::Combine($(SolutionDir), "packages")) + + + $(SolutionDir).nuget + packages.config + $(SolutionDir)packages + + + $(NuGetToolsPath)\nuget.exe + "$(NuGetExePath)" + mono --runtime=v4.0.30319 $(NuGetExePath) + + $(TargetDir.Trim('\\')) + + + "" + + + false + + + false + + + $(NuGetCommand) install "$(PackagesConfig)" -source $(PackageSources) -o "$(PackagesDir)" + $(NuGetCommand) pack "$(ProjectPath)" -p Configuration=$(Configuration) -o "$(PackageOutputDir)" -symbols + + + + RestorePackages; + $(BuildDependsOn); + + + + + $(BuildDependsOn); + BuildPackage; + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Lasy.nunit b/Lasy.nunit new file mode 100644 index 0000000..702599f --- /dev/null +++ b/Lasy.nunit @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/Lasy.nuspec b/Lasy.nuspec new file mode 100644 index 0000000..ff9f8be --- /dev/null +++ b/Lasy.nuspec @@ -0,0 +1,26 @@ + + + + Lasy + 1.2.0.8 + Lasy + Trinity Western University + Brian DeJong + false + A lightweight CRUD abstraction layer for data storage. + + Copyright 2014 + + + + + + + + + + + + + + diff --git a/Lasy.sln b/Lasy.sln new file mode 100644 index 0000000..02b93af --- /dev/null +++ b/Lasy.sln @@ -0,0 +1,40 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Express 2013 for Web +VisualStudioVersion = 12.0.21005.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lasy", "src\Lasy\Lasy.csproj", "{99A415F9-8D5A-4977-AC8B-86EA82C891D3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lasy.Tests", "test\Lasy.Tests\Lasy.Tests.csproj", "{088A595B-4550-475E-B1D0-47CB70FBDE6E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{ECC3DC6F-4EBA-4E2A-BE09-D7EAC57D1101}" + ProjectSection(SolutionItems) = preProject + .nuget\NuGet.exe = .nuget\NuGet.exe + .nuget\NuGet.targets = .nuget\NuGet.targets + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lasy-net4", "src\Lasy-net4\Lasy-net4.csproj", "{1D26E9C4-24C7-4D6A-8C47-F608F6314F58}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {99A415F9-8D5A-4977-AC8B-86EA82C891D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {99A415F9-8D5A-4977-AC8B-86EA82C891D3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {99A415F9-8D5A-4977-AC8B-86EA82C891D3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {99A415F9-8D5A-4977-AC8B-86EA82C891D3}.Release|Any CPU.Build.0 = Release|Any CPU + {088A595B-4550-475E-B1D0-47CB70FBDE6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {088A595B-4550-475E-B1D0-47CB70FBDE6E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {088A595B-4550-475E-B1D0-47CB70FBDE6E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {088A595B-4550-475E-B1D0-47CB70FBDE6E}.Release|Any CPU.Build.0 = Release|Any CPU + {1D26E9C4-24C7-4D6A-8C47-F608F6314F58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1D26E9C4-24C7-4D6A-8C47-F608F6314F58}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1D26E9C4-24C7-4D6A-8C47-F608F6314F58}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1D26E9C4-24C7-4D6A-8C47-F608F6314F58}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/lib/nunit.framework.dll b/lib/nunit.framework.dll deleted file mode 100644 index 6856e51..0000000 Binary files a/lib/nunit.framework.dll and /dev/null differ diff --git a/packages/repositories.config b/packages/repositories.config new file mode 100644 index 0000000..ba63e9e --- /dev/null +++ b/packages/repositories.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/FakeDB.cs b/src/FakeDB.cs deleted file mode 100644 index ecaa2be..0000000 --- a/src/FakeDB.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Lasy; -using Nvelope; - -namespace Lasy -{ - public class FakeDB : IReadable, IWriteable, IReadWrite - { - public Dictionary DataStore = new Dictionary(); - - public FakeDBTable Table(string tableName) - { - if (DataStore.ContainsKey(tableName)) - return DataStore[tableName]; - else - return new FakeDBTable(); - } - - public IEnumerable> RawRead(string tableName, Dictionary id, ITransaction transaction = null) - { - if (!DataStore.ContainsKey(tableName)) - return new List>(); - - return DataStore[tableName].FindByFieldValues(id); - } - - public IEnumerable> RawReadCustomFields(string tableName, IEnumerable fields, Dictionary id, ITransaction transaction = null) - { - return DataStore[tableName].FindByFieldValues(id).Select(row => row.WhereKeys(key => fields.Contains(key))); - } - - public IEnumerable> RawReadAll(string tableName, ITransaction transaction = null) - { - if (!DataStore.ContainsKey(tableName)) - return new List>(); - - return DataStore[tableName]; - } - - public IEnumerable> RawReadAllCustomFields(string tableName, IEnumerable fields, ITransaction transaction = null) - { - return DataStore[tableName].Select(row => row.WhereKeys(key => fields.Contains(key))); - } - - private IDBAnalyzer _analyzer = new FakeDBAnalyzer(); - - public IDBAnalyzer Analyzer - { - get { return _analyzer; } - set { _analyzer = value; } - } - - public Dictionary Insert(string tableName, Dictionary row, ITransaction transaction = null) - { - if (!DataStore.ContainsKey(tableName)) - DataStore.Add(tableName, new FakeDBTable()); - - var table = DataStore[tableName]; - - var dictToUse = row.Copy(); - //var id = DataStore[tableName].Count + 1; - var primaryKeys = Analyzer.GetPrimaryKeys(tableName); - var autoKey = Analyzer.GetAutoNumberKey(tableName); - - if (autoKey != null) - { - if (!dictToUse.ContainsKey(autoKey)) - dictToUse.Add(autoKey, table.NextAutoKey++); - else - dictToUse[autoKey] = table.NextAutoKey++; - } - - var invalid = primaryKeys.Where(key => dictToUse[key] == null); - if (invalid.Any()) - throw new KeyNotSetException(tableName, invalid); - - table.Add(dictToUse); - return dictToUse.WhereKeys(key => primaryKeys.Contains(key)); - } - - public void Delete(string tableName, Dictionary fieldValues, ITransaction transaction = null) - { - if (DataStore.ContainsKey(tableName)) - { - var victims = DataStore[tableName].FindByFieldValues(fieldValues).ToList(); - victims.ForEach(x => DataStore[tableName].Remove(x)); - } - } - - public void Update(string tableName, Dictionary dataFields, Dictionary keyFields, ITransaction transaction = null) - { - if(!DataStore.ContainsKey(tableName)) - return; - - var victims = DataStore[tableName].Where(r => r.IsSameAs(keyFields, keyFields.Keys)) - .Where(r => r != dataFields && r != keyFields); // Don't update if we've passed in the object itself, - // because at that point the change has already been made by a sneaky back-door reference change, - // and if we try to modify it here, we'll modify the collection while iterating over it, causing an exception - // The non-hacky fix would be to return copies of the rows from the Read methods. That way, the user couldn't - // make sneaky back-door changes. However, this would be a substantial performance penalty. Grr, I want - // Clojure's persistent collections here... - foreach (var vic in victims) - foreach (var key in dataFields.Keys) - vic[key] = dataFields[key]; - } - - public ITransaction BeginTransaction() - { - return new FakeDBTransaction(); - } - } -} diff --git a/src/FakeDBTable.cs b/src/FakeDBTable.cs deleted file mode 100644 index 4081598..0000000 --- a/src/FakeDBTable.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Nvelope; - -namespace Lasy -{ - public class FakeDBTable : List> - { - public IEnumerable> FindByFieldValues(Dictionary values) - { - return this.Where(x => filter(x, values)); - } - - private bool filter(Dictionary row, Dictionary values) - { - return values.Keys.All(field => row.ContainsKey(field) && row[field].Eq(values[field])); - } - - public int NextAutoKey = 1; - } -} diff --git a/src/FakeDBTransaction.cs b/src/FakeDBTransaction.cs deleted file mode 100644 index d4aa592..0000000 --- a/src/FakeDBTransaction.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Nvelope; - -namespace Lasy -{ - /// - /// Doesn't actually work. TODO: Implement this - /// - public class FakeDBTransaction : ITransaction - { - public void Commit() - { - } - - public void Rollback() - { - } - } -} diff --git a/src/IReadable.cs b/src/IReadable.cs deleted file mode 100644 index 31ba4c4..0000000 --- a/src/IReadable.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace Lasy -{ - public interface IReadable - { - /// - /// Read all that match on id from the table - /// - /// - /// - /// - /// - IEnumerable> RawRead(string tableName, Dictionary id, ITransaction transaction = null); - /// - /// Read just the specified fields from the table, filtering down to just the rows that match on id - /// - /// - /// - /// - /// - /// - IEnumerable> RawReadCustomFields(string tableName, IEnumerable fields, Dictionary id, ITransaction transaction = null); - /// - /// Get the analyzer for the DB - /// - IDBAnalyzer Analyzer { get; } - /// - /// Read all the rows from a table - /// - /// - /// - /// - IEnumerable> RawReadAll(string tableName, ITransaction transaction = null); - /// - /// Read just the specified fields from the table, but for all rows - /// - /// - /// - /// - /// - IEnumerable> RawReadAllCustomFields(string tableName, IEnumerable fields, ITransaction transaction = null); - } -} diff --git a/src/IWriteable.cs b/src/IWriteable.cs deleted file mode 100644 index 9655a43..0000000 --- a/src/IWriteable.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace Lasy -{ - public interface IWriteable - { - Dictionary Insert(string tableName, Dictionary row, ITransaction transaction = null); - - void Delete(string tableName, Dictionary row, ITransaction transaction = null); - - void Update(string tableName, Dictionary dataFields, Dictionary keyFields, ITransaction transaction = null); - - IDBAnalyzer Analyzer { get; } - - ITransaction BeginTransaction(); - } -} diff --git a/src/Lasy-net4/Lasy-net4.csproj b/src/Lasy-net4/Lasy-net4.csproj new file mode 100644 index 0000000..5960d53 --- /dev/null +++ b/src/Lasy-net4/Lasy-net4.csproj @@ -0,0 +1,131 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {1D26E9C4-24C7-4D6A-8C47-F608F6314F58} + Library + Properties + Lasy + Lasy + v4.0 + 512 + ..\..\Lasy\ + true + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + bin\Release\Lasy.XML + + + + False + ..\..\packages\MySql.Data.6.7.4\lib\net40\MySql.Data.dll + + + False + ..\..\packages\Newtonsoft.Json.6.0.1\lib\net40\Newtonsoft.Json.dll + + + False + ..\..\packages\Nvelope.1.1.0.2\lib\net40\Nvelope.dll + + + + + ..\..\packages\Rx-Core.2.2.2\lib\net40\System.Reactive.Core.dll + + + ..\..\packages\Rx-Interfaces.2.2.2\lib\net40\System.Reactive.Interfaces.dll + + + ..\..\packages\Rx-Linq.2.2.2\lib\net40\System.Reactive.Linq.dll + + + ..\..\packages\Rx-PlatformServices.2.2.3\lib\net40\System.Reactive.PlatformServices.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Code + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Lasy-net4/packages.config b/src/Lasy-net4/packages.config new file mode 100644 index 0000000..09b8afe --- /dev/null +++ b/src/Lasy-net4/packages.config @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/Lasy.csproj b/src/Lasy.csproj deleted file mode 100644 index 815e51e..0000000 --- a/src/Lasy.csproj +++ /dev/null @@ -1,80 +0,0 @@ - - - - Debug - AnyCPU - 8.0.30703 - 2.0 - {99A415F9-8D5A-4977-AC8B-86EA82C891D3} - Library - Properties - Lasy - Lasy - v4.0 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {AC4A93B6-DDB6-4FE1-B528-665DE101052B} - Nvelope - - - - - \ No newline at end of file diff --git a/src/Lasy/AbstractSqlReadWrite.cs b/src/Lasy/AbstractSqlReadWrite.cs new file mode 100644 index 0000000..bb0b79e --- /dev/null +++ b/src/Lasy/AbstractSqlReadWrite.cs @@ -0,0 +1,227 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Nvelope; + +namespace Lasy +{ + public abstract class AbstractSqlReadWrite : IReadWrite + { + public AbstractSqlReadWrite(string connectionString, SqlAnalyzer analyzer, bool strictTables = true) + { + SqlAnalyzer = analyzer; + ConnectionString = connectionString; + StrictTables = strictTables; + } + + public string ConnectionString { get; protected set; } + + protected abstract IEnumerable> sqlRead(string sql, Dictionary values); + protected abstract int? sqlInsert(string sql, Dictionary values); + protected abstract void sqlUpdate(string sql, Dictionary values); + + public virtual string QualifiedTable(string tablename) + { + var schema = SqlAnalyzer.SchemaName(tablename); + var table = SqlAnalyzer.TableName(tablename); + + if (schema.IsNullOrEmpty()) + return "[" + tablename + "]"; + else + return "[" + schema + "].[" + table + "]"; + } + + /// + /// Warning: You should greatly prefer using SQL parameters instead of using literals. + /// Literals are vulnerable to SQL injection attacks + /// + /// + /// + public static string SqlLiteral(object o) + { + if (o == null || o == DBNull.Value) + return "null"; + if (o is string || o is DateTime) + return "'" + o.ToString().Replace("'", "''") + "'"; + else + return o.ToString(); + } + + /// + /// If true, throw an exception when referencing tables that don't exist. + /// If false, do something intelligent instead - Reads return nothing, updates and + /// deletes do nothing, but inserts still throw exceptions + /// + public virtual bool StrictTables { get; set; } + + public virtual string MakeWhereClause(Dictionary keyFields, string paramPrefix = "", bool useParameters = true) + { + keyFields = keyFields ?? new Dictionary(); + + // TODO: Figure out how to pass null values as parameters + // instead of hardcoding them in here + var nullFields = keyFields.WhereValues(v => v == DBNull.Value || v == null).Keys; + var nullFieldParts = nullFields.Select(x => x + " is null"); + + var nonNullFields = keyFields.Except(nullFields).Keys; + var nonNullFieldParts = + useParameters ? + nonNullFields.Select(x => x + " = @" + paramPrefix + x) : + nonNullFields.Select(x => x + " = " + SqlLiteral(keyFields[x])); + + var whereClause = ""; + if (keyFields.Any()) + whereClause = " WHERE " + nullFieldParts.And(nonNullFieldParts).Join(" AND "); + + return whereClause; + } + + protected Dictionary _coerceToTableTypes(string tableName, Dictionary data) + { + var fieldTypes = SqlAnalyzer.GetFieldTypes(tableName); + return data.Select(kv => new KeyValuePair( + kv.Key, + data[kv.Key].ConvertTo(SqlTypeConversion.GetDotNetType(fieldTypes[kv.Key])))) + .ToDictionary(); + } + + public virtual string MakeReadSql(string tableName, Dictionary keyFields, IEnumerable fields = null, bool useParameters = true) + { + fields = fields ?? new string[]{}; + + var fieldClause = "*"; + if (fields.Any()) + fieldClause = fields.Join(", "); + + var coercedKeys = _coerceToTableTypes(tableName, keyFields); + var whereClause = MakeWhereClause(coercedKeys, "", useParameters); + + var sql = "SELECT " + fieldClause + " FROM " + QualifiedTable(tableName) + whereClause; + + return sql; + } + + public virtual string GetInsertedAutonumber() + { + return "SELECT SCOPE_IDENTITY()"; + } + + public virtual string MakeInsertSql(string tableName, Dictionary row, bool useParameters = true, bool selectIdentity = true) + { + //Retrieve the AutoNumbered key name if there is one + var autoNumberKeyName = Analyzer.GetAutoNumberKey(tableName); + + // Keep in mind that GetFields might return an empty list, which means that it doesn't know + var dbFields = Analyzer.GetFields(tableName).Or(row.Keys); + // Take out the autonumber keys, and take out any supplied data fields + // that aren't actually fields in the DB + var fieldNames = row.Keys.Except(autoNumberKeyName) + .Intersect(dbFields); + + var valList = useParameters ? + fieldNames.Select(x => "@" + x) : + fieldNames.Select(x => SqlLiteral(row[x])); + + var sql = "INSERT INTO " + QualifiedTable(tableName) + " (" + fieldNames.Join(", ") + ") " + + "VALUES (" + valList.Join(", ") + ")\n"; + if (selectIdentity) + sql += GetInsertedAutonumber(); + + return sql; + } + + public virtual string MakeUpdateSql(string tableName, Dictionary dataFields, Dictionary keyFields, bool useParameters = true) + { + var autoKey = Analyzer.GetAutoNumberKey(tableName); + + var setFields = dataFields.Keys.Except(autoKey); + var dbFields = Analyzer.GetFields(tableName); + // Don't try to set fields that don't exist in the database + if (dbFields.Any()) // If we don't get anything back, that means we don't know what the DB fields are + setFields = setFields.Intersect(dbFields); + + var whereClause = MakeWhereClause(keyFields, "key", useParameters); + + var valFields = + useParameters ? + setFields.Select(x => x + " = @data" + x) : + setFields.Select(x => x + " = " + SqlLiteral(dataFields[x])); + + var sql = "UPDATE " + QualifiedTable(tableName) + " SET " + valFields.Join(", ") + "\n" + whereClause; + return sql; + } + + public virtual string MakeDeleteSql(string tableName, Dictionary keyFields, bool useParameters = true) + { + var whereClause = MakeWhereClause(keyFields, "", useParameters); + return "DELETE FROM " + QualifiedTable(tableName) + whereClause; + } + + public virtual IEnumerable> RawRead(string tableName, Dictionary keyFields, IEnumerable fields = null) + { + // If the table doesn't exist, we probably don't want to run any sql + if (!Analyzer.TableExists(tableName)) + if (StrictTables) + throw new NotATableException(tableName); + else + return new List>(); + + var sql = MakeReadSql(tableName, keyFields, fields); + return sqlRead(sql, keyFields); + } + + public IDBAnalyzer Analyzer { get { return SqlAnalyzer; } } + public SqlAnalyzer SqlAnalyzer { get; protected set; } + + public virtual Dictionary Insert(string tableName, Dictionary row) + { + if (StrictTables && !Analyzer.TableExists(tableName)) + throw new NotATableException(tableName); + + // Make sure all the required keys are supplied + this.AssertInsertKeys(tableName, row); + + var sql = MakeInsertSql(tableName, row); + var autoKey = sqlInsert(sql, row); + + // If there's an autonumber, make sure we add it to the result + var autoNumberKeyName = Analyzer.GetAutoNumberKey(tableName); + //if (autoNumberKeyName != null && autoKey == null) + // throw new ThisSadlyHappenedException("The SQL ran beautifully, but you were expecting an autogenerated number and you did not get it"); + if(autoNumberKeyName != null && autoKey.HasValue) + row = row.Assoc(autoNumberKeyName, autoKey.Value); + + // Return the set of primary keys from the insert operation + var primaryKeys = Analyzer.GetPrimaryKeys(tableName); + return row.Only(primaryKeys); + } + + public virtual void Delete(string tableName, Dictionary keyFields) + { + if (!Analyzer.TableExists(tableName)) + if (StrictTables) + throw new NotATableException(tableName); + else + return; + + sqlUpdate(MakeDeleteSql(tableName, keyFields), keyFields); + } + + public virtual void Update(string tableName, Dictionary dataFields, Dictionary keyFields) + { + if (!Analyzer.TableExists(tableName)) + if (StrictTables) + throw new NotATableException(tableName); + else + return; + + var sql = MakeUpdateSql(tableName, dataFields, keyFields); + + var data = dataFields.SelectKeys(key => "data" + key); + var keys = keyFields.SelectKeys(key => "key" + key); + + sqlUpdate(sql, data.Union(keys)); + } + } +} diff --git a/src/Lasy/ConnectTo.cs b/src/Lasy/ConnectTo.cs new file mode 100644 index 0000000..a764e97 --- /dev/null +++ b/src/Lasy/ConnectTo.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Lasy +{ + /// + /// A bunch of helper methods to instantiate various database types + /// + public static class ConnectTo + { + public static SqlDB Sql2000(string connString, bool strictTables = true) + { + return new SqlDB(connString, new Sql2000Analyzer(connString), strictTables); + } + + public static SqlDB Sql2005(string connString, bool strictTables = true) + { + return new SqlDB(connString, new SqlAnalyzer(connString), strictTables); + } + + public static SqlDB MySql(string connString, bool strictTables = true) + { + return new MySqlDB(connString, new MySqlAnalyzer(connString), strictTables); + } + + public static ModifiableSqlDB ModifiableSql2000(string connString, ITypedDBAnalyzer taxonomy = null) + { + var analyzer = new Sql2000Analyzer(connString); + var modifier = new SqlModifier(connString, analyzer, taxonomy); + var db = new SqlDB(connString, analyzer, false); + return new ModifiableSqlDB(db, modifier); + } + + public static ModifiableSqlDB ModifiableSql2005(string connString, ITypedDBAnalyzer taxonomy = null) + { + var analyzer = new SqlAnalyzer(connString); + var modifier = new SqlModifier(connString, analyzer, taxonomy); + var db = new SqlDB(connString, analyzer, false); + return new ModifiableSqlDB(db, modifier); + } + + public static ModifiableSqlDB ModifiableMySql(string connString, ITypedDBAnalyzer taxonomy = null) + { + var analyzer = new MySqlAnalyzer(connString); + var modifier = new MySqlModifier(connString, analyzer, taxonomy); + var db = new MySqlDB(connString, analyzer, false); + return new ModifiableSqlDB(db, modifier); + } + + public static FileDB File(string directory, string fileExtension = ".rpt") + { + return new FileDB(directory, fileExtension); + } + + public static FakeDB Memory() + { + return new FakeDB(); + } + + public static UnreliableDb Unreliable() + { + return new UnreliableDb(); + } + } +} diff --git a/src/Lasy/DictionaryExtensions.cs b/src/Lasy/DictionaryExtensions.cs new file mode 100644 index 0000000..c81bd81 --- /dev/null +++ b/src/Lasy/DictionaryExtensions.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Nvelope; + +namespace Lasy +{ + public static class DictionaryExtensions + { + /// + /// Make sure that DBNull.Value is converted to null, so that we treat DBNull and null the same + /// on the backend + /// + /// + /// + public static Dictionary ScrubNulls(this Dictionary values) + { + var fields = values.Where(kv => kv.Value == DBNull.Value).Select(kv => kv.Key); + if (!fields.Any()) + return values; + var res = values.Copy(); + fields.Each(f => res[f] = null); + return res; + } + } +} diff --git a/src/Lasy/FakeDB.cs b/src/Lasy/FakeDB.cs new file mode 100644 index 0000000..bbcea57 --- /dev/null +++ b/src/Lasy/FakeDB.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Lasy; +using Nvelope; + +namespace Lasy +{ + public class FakeDB : ITransactable, IRWEvented + { + public FakeDB() + : this(new FakeDBMeta()) + { } + + public FakeDB(IDBAnalyzer analyzer) + { + Analyzer = analyzer; + } + + public Dictionary DataStore = new Dictionary(); + + public FakeDBTable Table(string tableName) + { + if (DataStore.ContainsKey(tableName)) + return DataStore[tableName]; + else + return new FakeDBTable(); + } + + public virtual void Wipe() + { + DataStore = new Dictionary(); + } + + public virtual IEnumerable> RawRead(string tableName, Dictionary keyFields, IEnumerable fields = null) + { + FireOnRead(tableName, keyFields); + + if (!DataStore.ContainsKey(tableName)) + return new List>(); + + return DataStore[tableName].Read(keyFields, fields); + } + + private IDBAnalyzer _analyzer = new FakeDBMeta(); + + public IDBAnalyzer Analyzer + { + get { return _analyzer; } + set { _analyzer = value; } + } + + public Dictionary NewAutokey(string tableName) + { + if (!DataStore.ContainsKey(tableName)) + DataStore.Add(tableName, new FakeDBTable()); + + var table = DataStore[tableName]; + + var autoKey = Analyzer.GetAutoNumberKey(tableName); + if (autoKey == null) + return new Dictionary(); + else + return new Dictionary() { { autoKey, table.NextAutoKey++ } }; + } + + public bool CheckKeys(string tableName, Dictionary row) + { + try + { + var res = this.ExtractKeys(tableName, row); + return true; + } + catch (KeyNotSetException) + { + return false; + } + } + + public virtual Dictionary Insert(string tableName, Dictionary row) + { + FireOnInsert(tableName, row); + + if (!DataStore.ContainsKey(tableName)) + DataStore.Add(tableName, new FakeDBTable()); + + var table = DataStore[tableName]; + + row = row.ScrubNulls(); + + var autoKeys = NewAutokey(tableName); + var dictToUse = row.Union(autoKeys); + CheckKeys(tableName, dictToUse); + table.Add(dictToUse); + + return this.ExtractKeys(tableName, dictToUse); + } + + public virtual void Delete(string tableName, Dictionary fieldValues) + { + FireOnDelete(tableName, fieldValues); + + if (DataStore.ContainsKey(tableName)) + { + fieldValues = fieldValues.ScrubNulls(); + var victims = DataStore[tableName].FindByFieldValues(fieldValues).ToList(); + victims.ForEach(x => DataStore[tableName].Remove(x)); + } + } + + public virtual void Update(string tableName, Dictionary dataFields, Dictionary keyFields) + { + FireOnUpdate(tableName, dataFields, keyFields); + + if(!DataStore.ContainsKey(tableName)) + return; + + dataFields = dataFields.ScrubNulls(); + keyFields = keyFields.ScrubNulls(); + + var victims = DataStore[tableName].Where(r => r.IsSameAs(keyFields, keyFields.Keys, null)); + foreach (var vic in victims) + foreach (var key in dataFields.Keys) + vic[key] = dataFields[key]; + } + + public virtual ITransaction BeginTransaction() + { + return new FakeDBTransaction(this); + } + + public void FireOnInsert(string tableName, Dictionary keyFields) + { + if (OnInsert != null) + OnInsert(tableName, keyFields); + if (OnWrite != null) + OnWrite(tableName, keyFields); + } + + public void FireOnDelete(string tableName, Dictionary fieldValues) + { + if (OnDelete != null) + OnDelete(tableName, fieldValues); + if (OnWrite != null) + OnWrite(tableName, fieldValues); + } + + public void FireOnUpdate(string tableName, Dictionary dataFields, Dictionary keyFields) + { + if (OnUpdate != null) + OnUpdate(tableName, dataFields, keyFields); + if (OnWrite != null) + OnWrite(tableName, dataFields.Union(keyFields)); + } + + public void FireOnRead(string tableName, Dictionary keyFields) + { + if (OnRead != null) + OnRead(tableName, keyFields); + } + + public event Action> OnInsert; + + public event Action> OnDelete; + + public event Action, Dictionary> OnUpdate; + + public event Action> OnWrite; + + public event Action> OnRead; + } +} diff --git a/src/FakeDBAnalyzer.cs b/src/Lasy/FakeDBMeta.cs similarity index 80% rename from src/FakeDBAnalyzer.cs rename to src/Lasy/FakeDBMeta.cs index cd27d98..34e9d4a 100644 --- a/src/FakeDBAnalyzer.cs +++ b/src/Lasy/FakeDBMeta.cs @@ -6,20 +6,20 @@ namespace Lasy { - public class FakeDBAnalyzer : IDBAnalyzer + public class FakeDBMeta : IDBAnalyzer { public Dictionary> PrimaryKeys = new Dictionary>(); public Dictionary AutoNumberKeys = new Dictionary(); + public Dictionary> Fields = new Dictionary>(); + /// /// If true, the analyzer will assume that there's a single autonumber PK for every table, /// that has the name [tableName]Id. Any additions to PrimaryKeys or AutoNumberKeys will override this /// public bool AssumeStandardKeys = true; - #region IDBAnalyzer Members - public ICollection GetPrimaryKeys(string tableName) { if (PrimaryKeys.ContainsKey(tableName)) @@ -38,7 +38,7 @@ public string GetAutoNumberKey(string tableName) else if (AssumeStandardKeys) return _unschemadTablename(tableName) + "Id"; else - throw new NotImplementedException("Dont know what the autonumbers for " + tableName + " would be. Either add that table's autonumbers to the AutoNumberKeys collection, or set AssumeStandardKeys to true for the default autonumber behavior"); + return null; } private string _unschemadTablename(string tablename) @@ -49,11 +49,18 @@ private string _unschemadTablename(string tablename) public ICollection GetFields(string tableName) { + // If we've been explicitly told the structure, use that + if (Fields.ContainsKey(tableName)) + return Fields[tableName]; + // We don't know what the actual structure is, so send back an empty // list to indicate that we don't actually know return new ReadOnlyCollection(new List { }); } - #endregion + public bool TableExists(string tableName) + { + return true; + } } } diff --git a/src/Lasy/FakeDBTable.cs b/src/Lasy/FakeDBTable.cs new file mode 100644 index 0000000..85038d0 --- /dev/null +++ b/src/Lasy/FakeDBTable.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Nvelope; + +namespace Lasy +{ + public class FakeDBTable : List> + { + public FakeDBTable() + : base() + { } + + public FakeDBTable(FakeDBTable coll) + : base(coll) + { + NextAutoKey = coll.NextAutoKey; + } + + public FakeDBTable(IEnumerable> rows, int nextAutokey) + : base(rows.ToList()) // Break any lazy evaluation so we don't end up re-evaluating the sequence + { + NextAutoKey = nextAutokey; + } + + public IEnumerable> FindByFieldValues(Dictionary values) + { + return this.Where(x => filter(x, values)); + } + + private bool filter(Dictionary row, Dictionary values) + { + return values.Keys.All(field => row.ContainsKey(field) && row[field].Eq(values[field])); + } + + public IEnumerable> Read(Dictionary values = null, IEnumerable fields = null) + { + fields = (fields ?? new List()).ToList(); + values = values ?? new Dictionary(); + + var rows = FindByFieldValues(values.ScrubNulls()); + if (!fields.Any()) + return rows.Select(r => r.Copy()).ToList(); + else + return rows.Select(row => row.WhereKeys(col => fields.Contains(col))).ToList(); + } + + public int NextAutoKey = 1; + } +} diff --git a/src/Lasy/FakeDBTransaction.cs b/src/Lasy/FakeDBTransaction.cs new file mode 100644 index 0000000..bb51b42 --- /dev/null +++ b/src/Lasy/FakeDBTransaction.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Nvelope; + +namespace Lasy +{ + /// + /// Fakes a transaction on a FakeDB + /// + /// Oh, the lies upon lies.... + public class FakeDBTransaction : ITransaction + { + public FakeDBTransaction(FakeDB db) + { + _db = db; + _operations = new List(); + } + + protected abstract class Op + { + public Op(string table) { Table = table; } + public string Table { get; set; } + public abstract FakeDBTable Apply(FakeDBTable table); + } + + protected class InsertOp : Op + { + /// + /// Note: row should include any autokeys that the table defines as well, + /// since InsertOp cannot determine them internally + /// + /// + /// + public InsertOp(string table, Dictionary row) + : base(table) + { + Row = row.Copy(); + } + + public Dictionary Row; + + public override FakeDBTable Apply(FakeDBTable table) + { + // We don't increment the NextAutoKey because that was assumed to have been done + // already when we created the InsertOp in the first place + return new FakeDBTable(table.And(Row), table.NextAutoKey); + } + } + + protected class UpdateOp : Op + { + public UpdateOp(string table, Dictionary data, Dictionary keys) + : base(table) + { + Keys = keys.Copy(); + NewValues = data.Copy(); + } + + public Dictionary Keys; + public Dictionary NewValues; + + public override FakeDBTable Apply(FakeDBTable table) + { + var victims = table.Where(r => Keys.IsSameAs(r)).ToList(); + var newVersions = victims.Select(r => r.Union(NewValues)); + return new FakeDBTable(table.Except(victims).And(newVersions), table.NextAutoKey); + } + } + + protected class DeleteOp : Op + { + public DeleteOp(string table, Dictionary keys) + : base(table) + { + Keys = keys.Copy(); + } + + public Dictionary Keys; + + public override FakeDBTable Apply(FakeDBTable table) + { + var victims = table.Where(r => Keys.IsSameAs(r)); + return new FakeDBTable(table.Except(victims), table.NextAutoKey); + } + } + + protected List _operations; + + protected FakeDB _db; + + public void Commit() + { + // Apply every operation in the transaction against the base database + foreach (var op in _operations) + _db.DataStore.Ensure(op.Table, op.Apply(_db.DataStore[op.Table])); + } + + public void Rollback() + { + // Don't need to do anything + } + + /// + /// Gets a filtered version of the table, having applied all of the operations of the transaction to it + /// + /// + /// + protected FakeDBTable _getTable(string table) + { + var underlying = _db.Table(table); + var opsForTable = _operations.Where(o => o.Table == table); + // Apply each of the operations to the table in sequence to get the output + var res = opsForTable.Aggregate(underlying, (source, op) => op.Apply(source)); + + return res; + } + + public IEnumerable> RawRead(string tableName, Dictionary keyFields, IEnumerable fields) + { + _db.FireOnRead(tableName, keyFields); + return _getTable(tableName).Read(keyFields, fields); + } + + public IDBAnalyzer Analyzer + { + get { return _db.Analyzer; } + } + + public Dictionary Insert(string tableName, Dictionary row) + { + _db.FireOnInsert(tableName, row); + + var autoKeys = _db.NewAutokey(tableName); + var inserted = row.ScrubNulls().Union(autoKeys); + + var pks = _db.ExtractKeys(tableName, inserted); + _operations.Add(new InsertOp(tableName, inserted)); + return pks; + } + + public void Delete(string tableName, Dictionary row) + { + _db.FireOnDelete(tableName, row); + _operations.Add(new DeleteOp(tableName, row.ScrubNulls())); + } + + public void Update(string tableName, Dictionary dataFields, Dictionary keyFields) + { + _db.FireOnUpdate(tableName, dataFields, keyFields); + _operations.Add(new UpdateOp(tableName, dataFields.ScrubNulls(), keyFields.ScrubNulls())); + } + + public void Dispose() + { + // Don't need to do anything + } + } +} diff --git a/src/FileDB.cs b/src/Lasy/FileDB.cs similarity index 60% rename from src/FileDB.cs rename to src/Lasy/FileDB.cs index 98ab304..ff732cb 100644 --- a/src/FileDB.cs +++ b/src/Lasy/FileDB.cs @@ -92,70 +92,28 @@ private IEnumerable> getTable(string tableName) private Dictionary convertRow(Dictionary row) { - return row.SelectVals(val => Infervert(val)); + return row.SelectVals(val => Nvelope.Reading.TypeConversion.Infervert(val)); } - /// - /// Used by Infervert, this is how we guess what type data is supposed to be - /// - private static Dictionary _infervertConversions = new Dictionary() - { - {new Regex("^[0-9]+$", RegexOptions.Compiled), typeof(int)}, - {new Regex("^[0-9]+\\.[0-9]+$", RegexOptions.Compiled), typeof(decimal)}, - {new Regex("^[tT]rue|[Ff]alse$", RegexOptions.Compiled), typeof(bool)}, - {new Regex("^[0-9]{4}\\-[0-9]{2}\\-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}$", - RegexOptions.Compiled), typeof(DateTime)}, - {new Regex(".*", RegexOptions.Compiled), typeof(string)} - }; - - /// - /// Based on a string representation, try to convert the value to the "appropriate" type - /// Largely guesswork - /// - /// - /// - public object Infervert(string value) - { - if (value == "NULL") - return null; - - var outputType = _infervertConversions.First(kv => kv.Key.IsMatch(value)).Value; - return value.ConvertTo(outputType); - } - - #region IReadable Members - - public IEnumerable> RawRead(string tableName, Dictionary id, ITransaction transaction = null) - { - var table = RawReadAll(tableName, transaction); - var results = table.Where(row => row.IsSameAs(id, id.Keys)); - return results; - } - - public IEnumerable> RawReadCustomFields(string tableName, IEnumerable fields, Dictionary id, ITransaction transaction = null) + public IEnumerable> RawRead(string tableName, Dictionary keyFields, IEnumerable fields) { - var res = RawRead(tableName, id, transaction); - return res.Select(r => r.WhereKeys(f => fields.Contains(f))); + fields = fields ?? new string[] { }; + keyFields = keyFields ?? new Dictionary(); + + var table = getTable(tableName); + var rows = table.Select(d => convertRow(d)); + if(keyFields.Any()) + rows = rows.Where(r => keyFields.IsSameAs(r)); + + if (fields.Any()) + return rows.Select(r => r.Only(fields)).ToList(); + else + return rows.ToList(); ; } public IDBAnalyzer Analyzer { get { throw new NotImplementedException(); } } - - public IEnumerable> RawReadAll(string tableName, ITransaction transaction = null) - { - var raw = getTable(tableName); - var table = raw.Select(d => convertRow(d)); - return table; - } - - public IEnumerable> RawReadAllCustomFields(string tableName, IEnumerable fields, ITransaction transaction = null) - { - var res = RawReadAll(tableName, transaction); - return res.Select(r => r.WhereKeys(f => fields.Contains(f))); - } - - #endregion } } diff --git a/src/Lasy/FunctionExtensions.cs b/src/Lasy/FunctionExtensions.cs new file mode 100644 index 0000000..d06f37b --- /dev/null +++ b/src/Lasy/FunctionExtensions.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Reactive; +using Nvelope; + +namespace Lasy +{ + /// + /// Encapsulates extension methods for Reactive-programming function extensions + /// + public static class FunctionExtensions + { + /// + /// + /// + /// We may be stretching the tradtional definition of Memoize with this one, but + /// the basic functionality is similar, so I haven't tried to come up with a better name + /// + /// + /// + /// + /// + /// + public static Func Memoize(this Func fn, TimeSpan cacheTimeout, IObservable cacheInvalidator) + { + // We'll stored things in a local cache so we don't hit the underlying fn every time + var cache = new Dictionary(); + // We want to watch to see if any cache invalidation events happen - if they do, we want to remove + // that value from the cache. + var cacheWatcher = cacheInvalidator.Subscribe(t => cache.Remove(t)); + // We also want to check to see if the cache has expired + // TODO: We can probably use the System.Reactive lib to wrap this up in a nicer + // way and combine it with the cachewatcher - either one should generate an event + // that clears the cache. + var inTime = Nvelope.FunctionExtensions.HasBeenCalledIn(cacheTimeout); + // Finally, return our function that has a built-in cache + return t => + { + // We need to call inTime every time to reset its timer, + // otherwise we'd just include it in the following if statement + var expired = !inTime(t); + if (!cache.ContainsKey(t) || expired) + cache.Ensure(t, fn(t)); + return cache[t]; + }; + } + } +} diff --git a/src/Lasy/IAnalyzable.cs b/src/Lasy/IAnalyzable.cs new file mode 100644 index 0000000..90ec071 --- /dev/null +++ b/src/Lasy/IAnalyzable.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Nvelope; +using Nvelope.Reflection; + +namespace Lasy +{ + public interface IAnalyzable + { + /// + /// Get the analyzer for the DB + /// + IDBAnalyzer Analyzer { get; } + } + + public static class IAnalyzableExtensions + { + /// + /// Gets all the primary keys for tablename from values. Throws an exception if any of the keys + /// are not supplied + /// + /// + /// + /// + /// + public static Dictionary ExtractKeys(this IAnalyzable writer, string tablename, + Dictionary values) + { + var keynames = writer.Analyzer.GetPrimaryKeys(tablename); + // Make sure they supplied all the keys + if (!values.Keys.ToSet().IsSupersetOf(keynames)) + throw new KeyNotSetException(tablename, keynames.Except(values.Keys)); + + var keys = values.Only(keynames); + return keys; + } + + /// + /// Make sure that values contains all the keys needed to insert into tablename. If not, + /// a KeyNotSetException will be thrown + /// + /// + /// + /// + public static void AssertInsertKeys(this IAnalyzable writer, string tablename, Dictionary values) + { + var keys = writer.Analyzer.GetPrimaryKeys(tablename).Except(writer.Analyzer.GetAutoNumberKey(tablename)); + if (!values.Keys.ToSet().IsSupersetOf(keys)) + throw new KeyNotSetException(tablename, keys.Except(values.Keys)); + } + } +} diff --git a/src/IDBAnalyzer.cs b/src/Lasy/IDBAnalyzer.cs similarity index 54% rename from src/IDBAnalyzer.cs rename to src/Lasy/IDBAnalyzer.cs index 8a40409..797a2d4 100644 --- a/src/IDBAnalyzer.cs +++ b/src/Lasy/IDBAnalyzer.cs @@ -15,6 +15,16 @@ public interface IDBAnalyzer /// /// ICollection GetFields(string tableName); + bool TableExists(string tableName); + } + public interface ITypedDBAnalyzer : IDBAnalyzer + { + /// + /// Gets the types of the fields for the given table + /// + /// + /// + Dictionary GetFieldTypes(string tablename, Dictionary example); } } diff --git a/src/Lasy/IDBModifier.cs b/src/Lasy/IDBModifier.cs new file mode 100644 index 0000000..2aebdba --- /dev/null +++ b/src/Lasy/IDBModifier.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Nvelope.Reflection; +using Nvelope; + +namespace Lasy +{ + public interface IDBModifier : IAnalyzable + { + /// + /// This analyzer should be able to tell the DBModifier about the structure + /// of any table that needs to be created + /// + ITypedDBAnalyzer Taxonomy { get; set; } + void CreateTable(string tablename, Dictionary fields); + void DropTable(string tablename); + } + + public static class IDBModifierExtensions + { + public static void CreateTable(this IDBModifier meta, string tablename, Dictionary instance) + { + var taxonomyTypes = meta.Taxonomy == null ? + new Dictionary() : + meta.Taxonomy.GetFieldTypes(tablename, instance); + + taxonomyTypes = taxonomyTypes ?? new Dictionary(); + + var missingTypes = instance.Except(taxonomyTypes.Keys) + .SelectVals(v => SqlTypeConversion.GetSqlType(v)); + + var fieldTypes = missingTypes.Union(taxonomyTypes); + + meta.CreateTable(tablename, fieldTypes); + } + + public static void CreateTable(this IDBModifier meta, string tablename, object instance) + { + CreateTable(meta, tablename, instance._AsDictionary()); + } + + public static void EnsureTable(this IDBModifier meta, string tablename, Dictionary instance) + { + if (!meta.Analyzer.TableExists(tablename)) + CreateTable(meta, tablename, instance); + } + + public static void EnsureTable(this IDBModifier meta, string tablename, object instance) + { + EnsureTable(meta, tablename, instance._AsDictionary()); + } + + public static void EnsureTable(this IDBModifier meta, string tablename, Dictionary fields) + { + if (!meta.Analyzer.TableExists(tablename)) + meta.CreateTable(tablename, fields); + } + + /// + /// Drops the table if it exists, else does nothing + /// + /// + /// + public static void KillTable(this IDBModifier meta, string tablename) + { + if (meta.Analyzer.TableExists(tablename)) + meta.DropTable(tablename); + } + } +} diff --git a/src/Lasy/IModifiable.cs b/src/Lasy/IModifiable.cs new file mode 100644 index 0000000..48b1fbc --- /dev/null +++ b/src/Lasy/IModifiable.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Nvelope; +using Nvelope.Reflection; + +namespace Lasy +{ + /// + /// Indicates that a database supports being modfiable - ie tables can be added at runtime + /// + public interface IModifiable : IAnalyzable + { + IDBModifier Modifier { get; } + } +} diff --git a/src/Lasy/INameQualifier.cs b/src/Lasy/INameQualifier.cs new file mode 100644 index 0000000..36271c1 --- /dev/null +++ b/src/Lasy/INameQualifier.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Lasy +{ + public interface INameQualifier + { + string TableName(string rawTablename); + string SchemaName(string rawTablename); + bool SupportsSchemas { get; } + } +} diff --git a/src/Lasy/IRWEvented.cs b/src/Lasy/IRWEvented.cs new file mode 100644 index 0000000..b881a1a --- /dev/null +++ b/src/Lasy/IRWEvented.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Lasy +{ + public interface IRWEvented : IReadWrite, IWriteEvented, IReadEvented + { } + + public interface IWriteEvented : IWriteable + { + /// + /// Fires before an insert + /// + event Action> OnInsert; + /// + /// Fires before a delete + /// + event Action> OnDelete; + /// + /// Fires before an update + /// + event Action, Dictionary> OnUpdate; + /// + /// Fires before every insert, update, or delete + /// + event Action> OnWrite; + } + + public interface IReadEvented : IReadable + { + /// + /// Fires before a Read operation + /// + event Action> OnRead; + } +} diff --git a/src/IReadWriteExtensions.cs b/src/Lasy/IReadWrite.cs similarity index 80% rename from src/IReadWriteExtensions.cs rename to src/Lasy/IReadWrite.cs index 9af1e28..f67f951 100644 --- a/src/IReadWriteExtensions.cs +++ b/src/Lasy/IReadWrite.cs @@ -2,11 +2,15 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using Nvelope.Reflection; using Nvelope; +using Nvelope.Reflection; namespace Lasy { + public interface IReadWrite : IReadable, IWriteable + { + } + public static class IReadWriteExtensions { /// @@ -20,18 +24,17 @@ public static class IReadWriteExtensions public static void Ensure(this IReadWrite readWrite, string tablename, Dictionary dataFields, - Dictionary keyFields, - ITransaction trans = null) + Dictionary keyFields) { // See if the keyFields exist // If so, update them, otherwise insert them - var existing = readWrite.Read(tablename, keyFields, trans); + var existing = readWrite.Read(tablename, keyFields); if (existing.Any()) - readWrite.Update(tablename, dataFields, keyFields, trans); + readWrite.Update(tablename, dataFields, keyFields); else { var newRow = dataFields.Union(keyFields); - var newKeys = readWrite.Insert(tablename, newRow, trans); + var newKeys = readWrite.Insert(tablename, newRow); } } @@ -46,14 +49,12 @@ public static void Ensure(this IReadWrite readWrite, public static void Ensure(this IReadWrite readWrite, string tablename, object dataObj, - object keyObj, - ITransaction trans = null) + object keyObj) { Ensure(readWrite, tablename, dataObj._AsDictionary(), - keyObj._AsDictionary(), - trans); + keyObj._AsDictionary()); } /// @@ -65,11 +66,10 @@ public static void Ensure(this IReadWrite readWrite, /// public static void Ensure(this IReadWrite readWrite, string tablename, - Dictionary values, - ITransaction trans = null) + Dictionary values) { var keyFields = readWrite.ExtractKeys(tablename, values); - Ensure(readWrite, tablename, values, keyFields, trans); + Ensure(readWrite, tablename, values, keyFields); } /// @@ -81,10 +81,9 @@ public static void Ensure(this IReadWrite readWrite, /// public static void Ensure(this IReadWrite readWrite, string tablename, - object valueObj, - ITransaction trans = null) + object valueObj) { - Ensure(readWrite, tablename, valueObj._AsDictionary(), trans); + Ensure(readWrite, tablename, valueObj._AsDictionary()); } } } diff --git a/src/IReadableExtensions.cs b/src/Lasy/IReadable.cs similarity index 51% rename from src/IReadableExtensions.cs rename to src/Lasy/IReadable.cs index a614304..86eb570 100644 --- a/src/IReadableExtensions.cs +++ b/src/Lasy/IReadable.cs @@ -2,24 +2,26 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using Nvelope; using Nvelope.Reflection; +using Nvelope; namespace Lasy { - public static class IReadableExtensions + public interface IReadable : IAnalyzable { - public static IEnumerable> RawReadCustomFields(this IReadable reader, string tableName, IEnumerable fields, object id, ITransaction transaction = null) - { - return reader.RawReadCustomFields(tableName, fields, id as Dictionary ?? id._AsDictionary(), transaction); - } - - public static IEnumerable> RawRead(this IReadable reader, string tableName, Dictionary id, ITransaction transaction = null) - { - return reader.RawRead(tableName, id as Dictionary ?? id._AsDictionary(), transaction); - } + /// + /// Read all that match on id from the table + /// + /// + /// + /// If supplied, read just these fields + /// + IEnumerable> RawRead(string tableName, Dictionary keyFields, IEnumerable fields = null); + } - public static Dictionary ReadPK(this IReadable reader, string tablename, int primaryKey, ITransaction transaction = null) + public static class IReadableExtensions + { + public static Dictionary ReadPK(this IReadable reader, string tablename, int primaryKey) { var keys = new Dictionary(); var keynames = reader.Analyzer.GetPrimaryKeys(tablename); @@ -27,38 +29,43 @@ public static Dictionary ReadPK(this IReadable reader, string ta if (keynames.Count > 1) throw new Exception("This table " + tablename + " has too many Primary Keys"); - var paras = new Dictionary(){{ keynames.First(), primaryKey}}; + var paras = new Dictionary() { { keynames.First(), primaryKey } }; - return ReadPK(reader, tablename, paras, transaction); + return ReadPK(reader, tablename, paras); } - public static Dictionary ReadPK(this IReadable reader, string tablename, Dictionary primaryKeys, ITransaction transaction = null) + public static Dictionary ReadPK(this IReadable reader, string tablename, Dictionary primaryKeys) { - var results = reader.RawRead(tablename, primaryKeys, transaction); + var results = reader.RawRead(tablename, primaryKeys); if (results.Count() > 1) throw new Exception("This table " + tablename + " has more than one row with the primary key " + primaryKeys.Print()); else if (results.Count() == 0) return null; else - return results.First(); + return results.First(); } - public static T ReadPK(this IReadable reader, string tablename, int primaryKey, ITransaction transaction = null) where T:class, new() + public static T ReadPK(this IReadable reader, string tablename, int primaryKey) where T : class, new() { - var results = reader.ReadPK(tablename, primaryKey, transaction); + var results = reader.ReadPK(tablename, primaryKey); var output = new T(); return output._SetFrom(results); } //Whatever we use for T needs to have a zero-parameter constructor - public static IEnumerable ReadAll(this IReadable reader, string tableName = null, ITransaction transaction = null) where T:class, new() + public static IEnumerable ReadAll(this IReadable reader, string tableName = null) where T : class, new() { - if(tableName.IsNullOrEmpty()) + if (tableName.IsNullOrEmpty()) tableName = typeof(T).Name; - var results = reader.RawReadAll(tableName, transaction); + var results = reader.RawRead(tableName, new Dictionary()); + + return results.Select(x => new T()._SetFrom(x)); + } - return results.Select(x => new T()._SetFrom(x) ); + public static IEnumerable> ReadAll(this IReadable reader, string tableName, IEnumerable fields = null) + { + return reader.RawRead(tableName, new Dictionary(), fields); } /// @@ -66,24 +73,23 @@ public static Dictionary ReadPK(this IReadable reader, string ta /// /// /// The name of the table - /// An object with values that are the "where" clause in sql. Example: new {col1 = 6, col2 = 'myString'} + /// An object with values that are the "where" clause in sql. Example: new {col1 = 6, col2 = 'myString'}. You can also pass a dictionarys /// A SQL transaction that can be passed in if this Read is to be part of a greater transaction /// - public static IEnumerable> Read(this IReadable reader, string tableName, object values, ITransaction trans = null) + public static IEnumerable> Read(this IReadable reader, string tableName, object values, IEnumerable fields = null) { - return reader.RawRead(tableName, values as Dictionary ?? values._AsDictionary(), trans); + return reader.RawRead(tableName, values as Dictionary ?? values._AsDictionary(), fields); } - public static IEnumerable Read(this IReadable reader, string tableName, object values, ITransaction trans = null) where T: class, new() + public static IEnumerable Read(this IReadable reader, string tableName, object values) where T : class, new() { - return Read(reader, tableName, values as Dictionary ?? values._AsDictionary(), trans); + return Read(reader, tableName, values as Dictionary ?? values._AsDictionary()); } - public static IEnumerable Read(this IReadable reader, string tableName, Dictionary values, ITransaction trans = null) where T:class, new() + public static IEnumerable Read(this IReadable reader, string tableName, Dictionary values) where T : class, new() { - var res = Read(reader, tableName, values, trans); - ObjectReader converter = new ObjectReader(); - return converter.ReadAll(res); + var results = Read(reader, tableName, values); + return results.Select(x => new T()._SetFrom(x)); } } } diff --git a/src/ITransaction.cs b/src/Lasy/ITransactable.cs similarity index 54% rename from src/ITransaction.cs rename to src/Lasy/ITransactable.cs index b11b1dd..152c342 100644 --- a/src/ITransaction.cs +++ b/src/Lasy/ITransactable.cs @@ -5,10 +5,8 @@ namespace Lasy { - public interface ITransaction + public interface ITransactable : IReadWrite { - void Commit(); - - void Rollback(); + ITransaction BeginTransaction(); } } diff --git a/src/Lasy/ITransaction.cs b/src/Lasy/ITransaction.cs new file mode 100644 index 0000000..fa79603 --- /dev/null +++ b/src/Lasy/ITransaction.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Lasy +{ + /// + /// Indicates that something is a transaction. + /// + public interface ITransaction : IReadWrite, IDisposable + { + void Commit(); + + void Rollback(); + } +} diff --git a/src/IWritableExtensions.cs b/src/Lasy/IWriteable.cs similarity index 54% rename from src/IWritableExtensions.cs rename to src/Lasy/IWriteable.cs index d381659..cf66e7a 100644 --- a/src/IWritableExtensions.cs +++ b/src/Lasy/IWriteable.cs @@ -1,40 +1,31 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; -using Nvelope; +using System.Text; using Nvelope.Reflection; +using Nvelope; namespace Lasy { - public static class IWritableExtensions + public interface IWriteable : IAnalyzable { - public static Dictionary Insert(this IWriteable writer, string tablename, object obj, ITransaction trans = null) - { - return writer.Insert(tablename, obj as Dictionary ?? obj._AsDictionary(), trans); - } + Dictionary Insert(string tableName, Dictionary row); + + void Delete(string tableName, Dictionary keyFields); + + void Update(string tableName, Dictionary dataFields, Dictionary keyFields); + } - public static int InsertAutoKey(this IWriteable writer, string tablename, object obj, ITransaction trans = null) + public static class IWriteableExtensions + { + public static Dictionary Insert(this IWriteable writer, string tablename, object obj) { - return writer.Insert(tablename, obj as Dictionary ?? obj._AsDictionary(), trans).Single().Value.ConvertTo(); + return writer.Insert(tablename, obj as Dictionary ?? obj._AsDictionary()); } - /// - /// Gets all the primary keys for tablename from values. Throws an exception if any of the keys - /// are not supplied - /// - /// - /// - /// - /// - public static Dictionary ExtractKeys(this IWriteable writer, string tablename, - Dictionary values) + public static int InsertAutoKey(this IWriteable writer, string tablename, object obj) { - var keynames = writer.Analyzer.GetPrimaryKeys(tablename); - // Make sure they supplied all the keys - if (!values.Keys.ToSet().IsSupersetOf(keynames)) - throw new KeyNotSetException(tablename, keynames.Except(values.Keys)); - - var keys = values.Only(keynames); - return keys; + return writer.Insert(tablename, obj as Dictionary ?? obj._AsDictionary()).Single().Value.ConvertTo(); } /// @@ -45,10 +36,10 @@ public static Dictionary ExtractKeys(this IWriteable writer, str /// /// /// - public static void Update(this IWriteable writer, string tablename, Dictionary values, ITransaction trans = null) + public static void Update(this IWriteable writer, string tablename, Dictionary values) { var keys = writer.ExtractKeys(tablename, values); - writer.Update(tablename, values, keys, trans); + writer.Update(tablename, values, keys); } /// @@ -59,11 +50,11 @@ public static void Update(this IWriteable writer, string tablename, Dictionary /// /// - public static void Update(this IWriteable writer, string tablename, object obj, ITransaction trans = null) + public static void Update(this IWriteable writer, string tablename, object obj) { var values = obj as Dictionary ?? obj._AsDictionary(); - Update(writer, tablename, values, trans); + Update(writer, tablename, values); } /// @@ -74,17 +65,17 @@ public static void Update(this IWriteable writer, string tablename, object obj, /// If Dict[string,object] it will be passed through, otherwise converted /// If Dict[string,object] it will be passed through, otherwise converted /// - public static void Update(this IWriteable writer, string tablename, object dataObj, object keysObj, ITransaction trans = null) + public static void Update(this IWriteable writer, string tablename, object dataObj, object keysObj) { - Dictionary data = dataObj as Dictionary ?? dataObj._AsDictionary(); - Dictionary keys = keysObj as Dictionary ?? keysObj._AsDictionary(); - writer.Update(tablename, data, keys, trans); + Dictionary data = dataObj as Dictionary ?? dataObj._AsDictionary(); + Dictionary keys = keysObj as Dictionary ?? keysObj._AsDictionary(); + writer.Update(tablename, data, keys); } - public static void Delete(this IWriteable writer, string tablename, object obj, ITransaction trans = null) + public static void Delete(this IWriteable writer, string tablename, object obj) { var dict = obj as Dictionary; - writer.Delete(tablename, dict ?? obj._AsDictionary(), trans); + writer.Delete(tablename, dict ?? obj._AsDictionary()); } } } diff --git a/src/Lasy/JsonDB.cs b/src/Lasy/JsonDB.cs new file mode 100644 index 0000000..3997bf3 --- /dev/null +++ b/src/Lasy/JsonDB.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Newtonsoft.Json; +using Nvelope; +using Nvelope.IO; +using Nvelope.Reading; + +namespace Lasy +{ + public class JsonDB : IReadWrite + { + public JsonDB(string filename, IDBAnalyzer analyzer = null) + { + Filename = filename; + Analyzer = analyzer ?? new FakeDBMeta(); + } + + public string Filename { get; protected set; } + + public IEnumerable> RawRead(string tableName, Dictionary keyFields, IEnumerable fields = null) + { + fields = fields ?? new string[] { }; + + var rows = _getTable(tableName); + var filtered = rows.Where(r => r.IsSameAs(keyFields, keyFields.Keys.Intersect(r.Keys), ObjectExtensions.LazyEq)); + var selected = fields.Any() ? + filtered.Select(r => r.Only(fields)) : + filtered; + + return selected; + } + + public IDBAnalyzer Analyzer { get; protected set; } + + public Dictionary Insert(string tableName, Dictionary row) + { + var allRows = _getTable(tableName); + var keys = _getKeys(tableName, row, allRows); + var prepedRow = row.Union(keys); + var toWrite = allRows.And(prepedRow); + _writeTable(tableName, toWrite); + return keys; + } + + public void Delete(string tableName, Dictionary keyFields) + { + var allRows = _getTable(tableName); + var victims = allRows.Where(r => r.IsSameAs(keyFields, keyFields.Keys.Intersect(r.Keys), ObjectExtensions.LazyEq)).ToList(); + var toWrite = allRows.Except(victims); + _writeTable(tableName, toWrite); + } + + public void Update(string tableName, Dictionary dataFields, Dictionary keyFields) + { + var allRows = _getTable(tableName); + var victims = allRows.Where(r => r.IsSameAs(keyFields, keyFields.Keys.Intersect(r.Keys), ObjectExtensions.LazyEq)).ToList(); + var updated = victims.Select(r => r.Union(dataFields)).ToList(); + var toWrite = allRows.Except(victims).And(updated); + _writeTable(tableName, toWrite); + } + + protected Dictionary _getKeys(string tablename, + Dictionary row, IEnumerable> existingRows) + { + var autoKey = Analyzer.GetAutoNumberKey(tablename); + if (autoKey == null) + return new Dictionary(); + + var existingKeys = existingRows.Select(r => r.Val(autoKey, null).ConvertTo() ?? 0); + var nextKey = existingKeys.Any() ? existingKeys.Max() + 1 : 1; + return new Dictionary() { { autoKey, nextKey } }; + } + + protected void _writeTable(string table, IEnumerable> allRows) + { + // Read everything and replace the file with it + var fullDb = _getTablenames().MapIndex(_getTable); + var toWrite = fullDb.Assoc(table, allRows); + // Write everything back to the file + var data = toWrite.Select(kv => _tableToS(kv.Key, kv.Value)).Flatten(); + TextFile.Spit(Filename, data.Join(Environment.NewLine)); + } + + protected IEnumerable _tableToS(string tablename, IEnumerable> allRows) + { + // If there's no rows, don't write anything + if (!allRows.Any()) + yield break; + + yield return tablename; + var rows = allRows.Select(r => JsonConvert.SerializeObject(r).Replace(Environment.NewLine, " ")); + foreach(var row in rows) + yield return row; + + yield return ""; // Include an empty line afterwards to make it pretty + } + + protected IEnumerable _getTablenames() + { + return _getAllLines().Where(_isTablename).Select(s => s.Trim()).ToList(); + } + + protected IEnumerable> _getTable(string tablename) + { + var allLines = _getAllLines(); + var tableStart = allLines.SkipWhile(l => l.Trim() != tablename) + .SkipWhile(l => l.Trim() == tablename); + var tableLines = tableStart.TakeWhile(_isNotTablename); + var rows = tableLines.Select(_toDict).ToList(); + return rows; + } + + protected Dictionary _toDict(string line) + { + var sd = JsonConvert.DeserializeObject>(line); + var od = sd.SelectVals(TypeConversion.Infervert); + return od; + } + + protected IEnumerable _getAllLines() + { + var lines = TextFile.Slurp(Filename).Split(Environment.NewLine).Where(s => !s.Trim().IsNullOrEmpty()); + return lines; + + } + + /// + /// Is the supplied file line a table name? + /// + /// + /// + protected bool _isTablename(string line) + { + return !line.Trim().StartsWith("{"); + } + + protected bool _isNotTablename(string line) + { + return !_isTablename(line); + } + } +} diff --git a/src/Lasy/Lasy.csproj b/src/Lasy/Lasy.csproj new file mode 100644 index 0000000..855bd00 --- /dev/null +++ b/src/Lasy/Lasy.csproj @@ -0,0 +1,131 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {99A415F9-8D5A-4977-AC8B-86EA82C891D3} + Library + Properties + Lasy + Lasy + v4.5.1 + 512 + ..\..\Lasy\ + true + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + bin\Release\Lasy.XML + + + + False + ..\..\packages\MySql.Data.6.7.4\lib\net40\MySql.Data.dll + + + False + ..\..\packages\Newtonsoft.Json.6.0.1\lib\net45\Newtonsoft.Json.dll + + + False + ..\..\packages\Nvelope.1.1.0.2\lib\net451\Nvelope.dll + + + + + ..\..\packages\Rx-Core.2.2.2\lib\net45\System.Reactive.Core.dll + + + ..\..\packages\Rx-Interfaces.2.2.2\lib\net45\System.Reactive.Interfaces.dll + + + ..\..\packages\Rx-Linq.2.2.2\lib\net45\System.Reactive.Linq.dll + + + ..\..\packages\Rx-PlatformServices.2.2.3\lib\net45\System.Reactive.PlatformServices.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Code + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Lasy/LasyExceptions.cs b/src/Lasy/LasyExceptions.cs new file mode 100644 index 0000000..cf25831 --- /dev/null +++ b/src/Lasy/LasyExceptions.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Nvelope; + +namespace Lasy +{ + public class KeyNotSetException : Exception + { + public KeyNotSetException(string tableName, IEnumerable keys) + : base("Could not get the keys for the table '" + tableName + "' because the following fields are null: " + keys.Print()) + { } + } + + public class ThisSadlyHappenedException : Exception + { + public ThisSadlyHappenedException(string message) + : base(message) + { } + } + + public class NotATableException : Exception + { + public NotATableException(string tablename, string message = null) + : base(message ?? ("The table '" + tablename + "' was not found in the database")) + { + Tablename = tablename; + } + + public string Tablename { get; private set; } + } + + public class MockDBFailure : Exception + { + public MockDBFailure() + : base() + { } + + public MockDBFailure(string message) + : base(message) + { } + } +} diff --git a/src/Lasy/LockBox.cs b/src/Lasy/LockBox.cs new file mode 100644 index 0000000..63cc019 --- /dev/null +++ b/src/Lasy/LockBox.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Nvelope; +using Nvelope.Reflection; + +namespace Lasy +{ + /// + /// Provides a disposable wrapper for locking rows in a database. When the object is + /// disposed, the rows are unlocked. Note: Assumes a LockId and LockDate field on + /// the table it's used on + /// + /// Best practice is to use this object in a using block, so you release + /// the locks as soon as your done your operations + public class LockBox : IEnumerable, IDisposable where T : class, new() + { + /// + /// + /// + /// + /// + /// An object containing key-value pairs of the rows to lock in the database. + /// For example {ShouldProcess = true} + /// If not supplied, will be DateTime.Now + public LockBox(IReadWrite db, string tablename, object criteria, DateTime? lockDate = null) + { + Db = db; + Tablename = tablename; + LockId = Guid.NewGuid().ToString(); + Criteria = criteria._AsDictionary(); + LockDate = lockDate ?? DateTime.Now; + } + + public string LockId { get; protected set; } + public IReadWrite Db { get; protected set; } + public string Tablename { get; protected set; } + public object Criteria { get; protected set; } + public DateTime LockDate { get; protected set; } + + private object _s_contentLock = new object(); + protected List _s_contents; + protected List _contents + { + get + { + if (_s_contents == null) + lock (_s_contentLock) + { + _s_contents = _lockRead(Criteria, LockDate).ToList(); + } + + return _s_contents; + } + set + { + _s_contents = value; + } + } + + /// + /// Read and lock some rows from the db. Only returns those rows that were successfully + /// locked. + /// + /// An object containing key-value pairs indicating the rows we want to return. + /// ie {Processed = false} + /// + /// + protected virtual IEnumerable _lockRead(object criteria, DateTime? lockDate = null) + { + lockDate = lockDate ?? DateTime.Now; + + // Only lock things that are not currently locked, + // otherwise we might end up whacking someone else's locks, + // which would be a Bad Thing (tm) + var lockCriteria = criteria._AsDictionary().Assoc("LockId", null); + + // Try to write the lock to the rows in the db + Db.Update(Tablename, new { LockId = LockId, LockDate = lockDate.Value }, lockCriteria); + + // Get all the rows that we successfully locked + // Don't use the lock date, just the lockId, because SQL Server will + // truncate datetimes, so the values won't match + var fromDb = _readLockedRows(Db, Tablename, lockCriteria.Assoc("LockId", LockId)); + + return fromDb; + } + + /// + /// This function is responsible for reading back the rows that we've already locked in whatever + /// format we need them in + /// + /// + /// + /// + /// + protected virtual IEnumerable _readLockedRows(IReadWrite db, string tablename, Dictionary lockCriteria) + { + return db.Read(tablename, lockCriteria); + } + + /// + /// Clears the lock + /// + /// The list of items that the box had locked + public virtual IEnumerable Unlock() + { + var res = _contents; + + // Clear the lockDate and LockId for any row that has this lockId + var updateData = new { LockId = DBNull.Value, LockDate = DBNull.Value }; + var keys = new { LockId = LockId }; + Db.Update(Tablename, updateData, keys); + + // Clear out the cache, so that we report that the box is empty + _contents = new List(); + + return res; + } + + public void Dispose() + { + Unlock(); + } + + public IEnumerator GetEnumerator() + { + foreach (var item in _contents) + yield return item; + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + public class EmptyLockBox : LockBox where T: class, new() + { + public EmptyLockBox() + : base(null, "", null, null) + { } + + public override IEnumerable Unlock() + { + yield break; + } + + protected override IEnumerable _lockRead(object criteria, DateTime? lockDate = null) + { + yield break; + } + } + + /// + /// Provides a disposable wrapper for locking rows in a database. When the object is + /// disposed, the rows are unlocked. Note: Assumes a LockId and LockDate field on + /// the table it's used on + /// + /// Best practice is to use this object in a using block, so you release + /// the locks as soon as your done your operations + public class LockBox : LockBox> + { + /// + /// + /// + /// + /// + /// An object containing key-value pairs of the rows to lock in the database. + /// For example {ShouldProcess = true} + /// If not supplied, will be DateTime.Now + public LockBox(IReadWrite db, string tablename, object criteria, DateTime? lockDate = null) + : base(db, tablename, criteria, lockDate) + { } + + protected override IEnumerable> _readLockedRows(IReadWrite db, string tablename, Dictionary lockCriteria) + { + return db.Read(tablename, lockCriteria); + } + } + + public class EmptyLockBox : LockBox + { + public EmptyLockBox() + : base(null, "", null, null) + { } + + public override IEnumerable> Unlock() + { + yield break; + } + + protected override IEnumerable> _lockRead(object criteria, DateTime? lockDate = null) + { + yield break; + } + } +} diff --git a/src/Lasy/ModifiableSqlDB.cs b/src/Lasy/ModifiableSqlDB.cs new file mode 100644 index 0000000..47038d0 --- /dev/null +++ b/src/Lasy/ModifiableSqlDB.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Nvelope; +using Nvelope.Reflection; + +namespace Lasy +{ + /// + /// A SqlDB that automatically creates tables (and schemas) as they are needed. + /// + /// In order for the table/schema creation to work, the connection string + /// needs to be for a dbo user on the database + public class ModifiableSqlDB : IReadWrite, ITransactable, IModifiable + { + public ModifiableSqlDB(SqlDB db, SqlModifier modifier) + { + SqlModifier = modifier; + DB = db; + } + + public SqlModifier SqlModifier { get; protected set; } + public IDBModifier Modifier { get { return SqlModifier; } } + public IDBAnalyzer Analyzer { get { return Modifier.Analyzer; } } + public SqlAnalyzer SqlAnalyzer { get { return Analyzer as SqlAnalyzer; } } + public SqlDB DB; + + + public Dictionary Insert(string tableName, Dictionary row) + { + // If the table doesn't exist when inserting, create it + if (!Analyzer.TableExists(tableName)) + Modifier.CreateTable(tableName, row); + + return DB.Insert(tableName, row); + } + + public ITransaction BeginTransaction() + { + return new ModifiableSqlDbTransaction(this); + } + + + public IEnumerable> RawRead(string tableName, Dictionary keyFields, IEnumerable fields = null) + { + return DB.RawRead(tableName, keyFields, fields); + } + + + public void Delete(string tableName, Dictionary keyFields) + { + DB.Delete(tableName, keyFields); + } + + public void Update(string tableName, Dictionary dataFields, Dictionary keyFields) + { + DB.Update(tableName, dataFields, keyFields); + } + } +} diff --git a/src/Lasy/ModifiableSqlDbTransaction.cs b/src/Lasy/ModifiableSqlDbTransaction.cs new file mode 100644 index 0000000..38d9fe4 --- /dev/null +++ b/src/Lasy/ModifiableSqlDbTransaction.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Nvelope; +using Nvelope.Reflection; + +namespace Lasy +{ + public class ModifiableSqlDbTransaction : SqlDBTransaction + { + public ModifiableSqlDB ModifiableDb; + public ModifiableSqlDbTransaction(ModifiableSqlDB db) + : base(db.DB) + { + ModifiableDb = db; + } + + public override Dictionary Insert(string tableName, Dictionary row) + { + if (!ModifiableDb.Analyzer.TableExists(tableName)) + ModifiableDb.Modifier.CreateTable(tableName, row); + + return base.Insert(tableName, row); + } + } +} diff --git a/src/Lasy/MySqlAnalyzer.cs b/src/Lasy/MySqlAnalyzer.cs new file mode 100644 index 0000000..c112410 --- /dev/null +++ b/src/Lasy/MySqlAnalyzer.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Data; +using MySql.Data.MySqlClient; +using System.Text.RegularExpressions; +using Nvelope; + +namespace Lasy +{ + public class MySqlAnalyzer : SqlAnalyzer + { + public MySqlAnalyzer(string connectionString, TimeSpan cacheDuration = default(TimeSpan)) + : base(connectionString, new MySqlNameQualifier(connectionString), cacheDuration) + { } + + protected internal override IDbConnection _getConnection(string connectionString) + { + return new MySqlConnection(connectionString); + } + + protected string _dbName + { + get + { + var match = Regex.Match(_connectionString, "[dD]atabase=([^;]+);"); + if (match.Success) + return match.Groups[1].Value; + else + throw new ApplicationException("Couldn't extract the database name from the connection string!"); + } + } + + protected internal override string _getTableExistsSql(string schema, string table) + { + // In MySQL, both tables and views show up in information_schema.tables, so we + // don't need to look at information_schema.views + // Note that "schema" in mySql means "database" in MS-SQL. They don't have schemas, so + // we just always use "" as the schema. HOWEVER, where it asks for schema here, we + // should be passing in the database name + return @"select 1 from information_schema.tables where table_name = @table and table_schema = '" + _dbName + "'"; + } + + protected internal override string _getPrimaryKeySql() + { + return @"SELECT k.COLUMN_NAME + FROM information_schema.table_constraints t + LEFT JOIN information_schema.key_column_usage k + USING(constraint_name,table_schema,table_name) + WHERE t.constraint_type='PRIMARY KEY' + AND t.table_schema = DATABASE() + AND t.table_name = @table"; + } + + protected internal override string _getAutonumberKeySql() + { + return @"select column_name from information_schema.columns + where table_name = @table and table_schema = Database() + and extra like '%auto_increment%'"; + } + + protected internal override string _getFieldTypeSql() + { + return @"select * from information_schema.columns + where table_name = @table and table_schema = Database()"; + } + + public override bool SchemaExists(string schema) + { + return schema.IsNullOrEmpty() || schema == SchemaName(""); + } + + public override string SchemaName(string tablename) + { + // MySql doesn't have schemas. The things it calls schemas + // are really databases + return ""; + } + } +} diff --git a/src/Lasy/MySqlDB.cs b/src/Lasy/MySqlDB.cs new file mode 100644 index 0000000..19ca08e --- /dev/null +++ b/src/Lasy/MySqlDB.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using MySql.Data.MySqlClient; +using Nvelope; +using System.Text.RegularExpressions; + +namespace Lasy +{ + public class MySqlDB : SqlDB + { + public MySqlDB(string connectionString, SqlAnalyzer analyzer, bool strictTables = true) + : base(connectionString, analyzer, strictTables) + { } + + protected internal override System.Data.IDbConnection _getConnection() + { + return new MySqlConnection(ConnectionString); + } + + public override string QualifiedTable(string tablename) + { + var schema = SqlAnalyzer.SchemaName(tablename); + var table = SqlAnalyzer.TableName(tablename); + + if (schema.IsNullOrEmpty()) + return tablename; + else + return "`" + schema + "`.`" + table + "`"; + } + + public override string GetInsertedAutonumber() + { + return "; SELECT LAST_INSERT_ID();"; + } + + #region Temporary Transaction fix +#warning Remove this once nested transaction issue fixed + + public override ITransaction BeginTransaction() + { + return new NonTransaction(this); + } + + public class NonTransaction : ITransaction + { + public NonTransaction(MySqlDB db) + { + _db = db; + } + + public MySqlDB _db; + + public void Commit() { } + + public void Rollback() { } + + public IEnumerable> RawRead(string tableName, Dictionary keyFields, IEnumerable fields = null) + { + return _db.RawRead(tableName, keyFields, fields); + } + + public IDBAnalyzer Analyzer + { + get { return _db.Analyzer; } + } + + public Dictionary Insert(string tableName, Dictionary row) + { + return _db.Insert(tableName, row); + } + + public void Delete(string tableName, Dictionary keyFields) + { + _db.Delete(tableName, keyFields); + } + + public void Update(string tableName, Dictionary dataFields, Dictionary keyFields) + { + _db.Update(tableName, dataFields, keyFields); + } + + public void Dispose() { } + } + + #endregion + } +} diff --git a/src/Lasy/MySqlModifier.cs b/src/Lasy/MySqlModifier.cs new file mode 100644 index 0000000..682cfd7 --- /dev/null +++ b/src/Lasy/MySqlModifier.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Nvelope; + +namespace Lasy +{ + public class MySqlModifier : SqlModifier + { + public MySqlModifier(string connectionString, SqlAnalyzer analyzer, ITypedDBAnalyzer taxonomy = null) + : base(connectionString, analyzer, taxonomy) + { } + + protected internal override System.Data.IDbConnection _getConnection(string connectionString) + { + return new MySql.Data.MySqlClient.MySqlConnection(connectionString); + } + + public override string _getCreateSchemaSql(string schema) + { + throw new InvalidOperationException("You can't create schemas in MySql"); + } + + public override string _getDropSchemaSql(string schema) + { + throw new InvalidOperationException("You can't drop schemas in MySql"); + } + + public override string _getCreateTableSql(string schema, string table, Dictionary fieldTypes) + { + // Strip off the primary key if it was supplied in fields - we'll make it ourselves + var datafields = fieldTypes.Except(table + "Id"); + var fieldList = _fieldDefinitions(datafields); + + var sql = String.Format(@"CREATE TABLE `{1}` + ( + {1}Id int NOT NULL AUTO_INCREMENT PRIMARY KEY, + {2} + )", + schema, table, fieldList); + + return sql; + } + + public override string _getDropTableSql(string schema, string table) + { + return string.Format("drop table `{1}`", schema, table); + } + + public override string _getDropViewSql(string schema, string view) + { + return string.Format("drop view `{1}`", schema, view); + } + } +} diff --git a/src/Lasy/MySqlNameQualifier.cs b/src/Lasy/MySqlNameQualifier.cs new file mode 100644 index 0000000..9facd83 --- /dev/null +++ b/src/Lasy/MySqlNameQualifier.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace Lasy +{ + public class MySqlNameQualifier : SqlNameQualifier + { + public static MySqlNameQualifier FromDbname(string schema) + { + var res = new MySqlNameQualifier("Database=" + schema + ";"); + return res; + } + + public MySqlNameQualifier(string connectionString) + { + // Extract the database name from the connection string, + // and always use that as the schema name, since it seems + // like mySql calls SqlServer "databases" "schemas", and + // doesn't have the concept of SqlServer schemas + var match = Regex.Match(connectionString, "Database=([^;]+);"); + if (!match.Success) + throw new Exception("Can't figure out the mySql schema name from the connection string. " + + "Expected to find Database=XXXX; in this connection string, but didn't: " + connectionString); + + _schema = match.Groups[1].Value; + } + + private string _schema; + + public override string SchemaName(string tablename) + { + return ""; + } + + public override bool SupportsSchemas + { + get + { + return false; + } + } + } +} diff --git a/src/Lasy/ObjectAnalyzer.cs b/src/Lasy/ObjectAnalyzer.cs new file mode 100644 index 0000000..74e923f --- /dev/null +++ b/src/Lasy/ObjectAnalyzer.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Nvelope; +using Nvelope.Reflection; + +namespace Lasy +{ + /// + /// An ITypedDBAnalyzer that reads structure from objects + /// + public class ObjectAnalyzer : ITypedDBAnalyzer + { + protected class ObjAnalyzerItem + { + public string Name; + public Dictionary Types; + } + + protected List _items = new List(); + + public void Add(string name, Dictionary fieldTypes) + { + _items.Add(new ObjAnalyzerItem() { Name = name, Types = fieldTypes }); + } + + public void Add(string name, object obj) + { + var types = obj._SqlFieldTypes(); + Add(name, types); + } + + public void Add(string name, Type type) + { + var types = type._SqlFieldTypes(); + Add(name, types); + } + + public Dictionary GetFieldTypes(string tablename, Dictionary example) + { + if (!TableExists(tablename)) + return null; + + var types = _items.First(i => i.Name == tablename).Types; + var extras = example.Except(types.Keys).SelectVals(v => SqlTypeConversion.GetSqlType(v)); + var res = extras.Union(types); + return res; + } + + public ICollection GetPrimaryKeys(string tableName) + { + return new string[] { }; + } + + public string GetAutoNumberKey(string tableName) + { + return null; + } + + public ICollection GetFields(string tableName) + { + if (TableExists(tableName)) + return _items.First(i => i.Name == tableName).Types.Keys; + else + return new string[] { }; + } + + public bool TableExists(string tableName) + { + return _items.Any(i => i.Name == tableName); + } + } +} diff --git a/src/Properties/.svn/entries b/src/Lasy/Properties/.svn/entries similarity index 100% rename from src/Properties/.svn/entries rename to src/Lasy/Properties/.svn/entries diff --git a/src/Properties/.svn/text-base/AssemblyInfo.cs.svn-base b/src/Lasy/Properties/.svn/text-base/AssemblyInfo.cs.svn-base similarity index 100% rename from src/Properties/.svn/text-base/AssemblyInfo.cs.svn-base rename to src/Lasy/Properties/.svn/text-base/AssemblyInfo.cs.svn-base diff --git a/src/Properties/AssemblyInfo.cs b/src/Lasy/Properties/AssemblyInfo.cs similarity index 78% rename from src/Properties/AssemblyInfo.cs rename to src/Lasy/Properties/AssemblyInfo.cs index 3e79856..5ef5027 100644 --- a/src/Properties/AssemblyInfo.cs +++ b/src/Lasy/Properties/AssemblyInfo.cs @@ -6,11 +6,11 @@ // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("Lasy")] -[assembly: AssemblyDescription("")] +[assembly: AssemblyDescription("A lightweight CRUD abstraction layer for data storage.")] [assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyCompany("Trinity Western University")] [assembly: AssemblyProduct("Lasy")] -[assembly: AssemblyCopyright("Copyright © Microsoft 2010")] +[assembly: AssemblyCopyright("Copyright © 2014")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyVersion("1.2.0.8")] +[assembly: AssemblyFileVersion("1.2.0.8")] diff --git a/src/Lasy/SQLAnalyzer.cs b/src/Lasy/SQLAnalyzer.cs new file mode 100644 index 0000000..16e84d9 --- /dev/null +++ b/src/Lasy/SQLAnalyzer.cs @@ -0,0 +1,287 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Nvelope; +using System.Data; +using System.Reactive.Linq; +using System.Data.Common; + +namespace Lasy +{ + public class SqlAnalyzer : ITypedDBAnalyzer + { + public SqlAnalyzer(string connectionString, INameQualifier nameQualifier = null, TimeSpan cacheDuration = default(TimeSpan)) + { + nameQualifier = nameQualifier ?? new SqlNameQualifier(); + + if(cacheDuration == default(TimeSpan)) + cacheDuration = _defaultCacheDuration(); + + _connectionString = connectionString; + NameQualifier = nameQualifier; + + // We use function references instead of directly exposing the functions so + // that we can build in caching without much work. + // We'll do caching using Memoize - it'll cache the results of the function + // for as long as cacheDuration. + // Also, in some subclasses (ie, SqlMetaAnalyzer), we implement a different + // caching scheme - using these function references lets us do that without + // having to change anything here. + + // Why didn't we just do this through polymorphism, you ask? + // Well, if we did, we wouldn't be able to compose our functions easily - we can't + // override a function and just say "hey, use a memoized version of this function instead + // of the base-class version" - we'd have to implement memoization from scratch in each + // method. That's just a silly waste of time. Also, when we subclass, we'd have to implement + // all of the memoization and cache invalidation we do in SqlMetaAnalyzer for each of these + // methods again! Polymorphism doesn't allow us to do any manipulation of our functions - all you + // can do is reimplement them, you can't get at the underlying binding and change it. That is to say, + // there's no way using override to say "replace this method with a memoized version of it" - all you + // can do is implement the guts of memoize inside your function, and repeat it for every function + // you want to do the same thing to. + var schemaEvents = Observable.FromEvent(d => OnInvalidateSchemaCache += d, d => OnInvalidateSchemaCache -= d); + var tableEvents = Observable.FromEvent(d => OnInvalidateTableCache += d, d => OnInvalidateTableCache -= d); + + _getAutonumberKey = new Func(_getAutonumberKeyFromDB).Memoize(cacheDuration, tableEvents); + _getFieldTypes = new Func>(_getFieldTypesFromDB).Memoize(cacheDuration, tableEvents); + _getPrimaryKeys = new Func>(_getPrimaryKeysFromDB).Memoize(cacheDuration, tableEvents); + _tableExists = new Func(_tableExistsFromDB).Memoize(cacheDuration, tableEvents); + _schemaExists = new Func(_schemaExistsFromDb).Memoize(cacheDuration, schemaEvents); + } + + protected virtual TimeSpan _defaultCacheDuration() + { + return new TimeSpan(0, 10, 0); + } + + protected Func> _getPrimaryKeys; + protected Func _getAutonumberKey; + protected Func> _getFieldTypes; + protected Func _tableExists; + protected Func _schemaExists; + protected string _connectionString; + public INameQualifier NameQualifier { get; private set; } + + protected event Action OnInvalidateTableCache; + protected event Action OnInvalidateSchemaCache; + /// + /// Call this to indicate that information for a cached table is no longer valid + /// + /// + public void InvalidateTableCache(string tablename) + { + if (OnInvalidateTableCache != null) + OnInvalidateTableCache(tablename); + } + /// + /// Call this to indicate that information for a cached schema is no longer valid + /// + /// + public void InvalidateSchemaCache(string schema) + { + if (OnInvalidateSchemaCache != null) + OnInvalidateSchemaCache(schema); + } + + protected internal virtual IDbConnection _getConnection(string connectionString) + { + return new System.Data.SqlClient.SqlConnection(connectionString); + } + + protected internal virtual string _getPrimaryKeySql() + { + return @"select isc.Column_name + from + sys.columns c inner join sys.tables t on c.object_id = t.object_id + inner join information_schema.columns isc + on schema_id(isc.TABLE_SCHEMA) = t.schema_id and isc.TABLE_NAME = t.name and isc.COLUMN_NAME = c.name + left join INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE cu + on cu.TABLE_SCHEMA = isc.TABLE_SCHEMA and cu.TABLE_NAME = isc.TABLE_NAME and cu.COLUMN_NAME = isc.COLUMN_NAME + left join INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc + on cu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME and tc.CONSTRAINT_TYPE = 'PRIMARY KEY' + where isc.TABLE_NAME = @table and isc.TABLE_SCHEMA = @schema and tc.CONSTRAINT_TYPE = 'PRIMARY KEY' + order by ORDINAL_POSITION"; + } + + protected internal virtual string _getAutonumberKeySql() + { + return @"select isc.Column_name + from + sys.columns c inner join sys.tables t on c.object_id = t.object_id + inner join information_schema.columns isc + on schema_id(isc.TABLE_SCHEMA) = t.schema_id and isc.TABLE_NAME = t.name and isc.COLUMN_NAME = c.name + left join INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE cu + on cu.TABLE_SCHEMA = isc.TABLE_SCHEMA and cu.TABLE_NAME = isc.TABLE_NAME and cu.COLUMN_NAME = isc.COLUMN_NAME + left join INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc + on cu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME and tc.CONSTRAINT_TYPE = 'PRIMARY KEY' + where isc.TABLE_NAME = @table and isc.TABLE_SCHEMA = @schema + and (is_identity = 1 or (tc.CONSTRAINT_TYPE = 'PRIMARY KEY' and isc.COLUMN_DEFAULT is not null)) + order by ORDINAL_POSITION"; + } + + protected internal virtual string _getTableExistsSql(string schema, string table) + { + //return "select 1 from sys.tables where name = @table union all select 1 from sys.views where name = @table"; + return @"SELECT 1 + FROM sys.tables + LEFT JOIN sys.schemas ON tables.schema_id = schemas.schema_id + WHERE tables.name = @table + AND schemas.name = @schema + UNION ALL + SELECT 1 + FROM sys.views + LEFT JOIN sys.schemas ON views.schema_id = schemas.schema_id + WHERE views.name = @table + AND schemas.name = @schema"; + } + + protected internal virtual string _getFieldTypeSql() + { + return @"SELECT + isc.* + FROM + sys.objects tbl + inner join sys.schemas schemas + on tbl.schema_id = schemas.schema_id + inner join sys.columns c + on tbl.object_id = c.object_id + inner join information_schema.columns isc + on isc.column_name = c.name and isc.table_name = tbl.name and isc.table_schema = schemas.name + left outer join information_schema.key_column_usage k + on k.table_name = tbl.name and objectproperty(object_id(constraint_name), 'IsPrimaryKey') = 1 + and k.column_name = c.name + WHERE + tbl.name = @table + and schemas.name = @schema + order by isc.ORDINAL_POSITION"; + } + + protected internal virtual string _getSchemaExistsSql() + { + return "select 1 from sys.schemas where name = @schema"; + } + + public ICollection GetPrimaryKeys(string tableName) + { + return _getPrimaryKeys(tableName); + } + + protected ICollection _getPrimaryKeysFromDB(string tableName) + { + using (var conn = _getConnection(_connectionString)) + { + return conn.ExecuteSingleColumn(_getPrimaryKeySql(), new { table = TableName(tableName), schema = SchemaName(tableName) }); + } + } + + public string GetAutoNumberKey(string tableName) + { + return _getAutonumberKey(tableName); + } + + protected string _getAutonumberKeyFromDB(string tableName) + { + using (var conn = _getConnection(_connectionString)) + { + var res = conn.ExecuteSingleColumn(_getAutonumberKeySql(), new { table = TableName(tableName), schema = SchemaName(tableName) }); + // Under certain circumstances, we get duplicated rows back. In taht situation, don't crash + return res.FirstOr(null); + } + } + + public Dictionary GetFieldTypes(string tablename, Dictionary example = null) + { + example = example ?? new Dictionary(); + + var exampleFields = example.SelectVals(v => SqlTypeConversion.GetSqlType(v)); + var sqlFields = _getFieldTypes(tablename); + var res = exampleFields.Union(sqlFields); + return res; + } + + public ICollection GetFields(string tableName) + { + return GetFieldTypes(tableName).Keys; + } + + protected Dictionary _getFieldTypesFromDB(string tableName) + { + using (var conn = _getConnection(_connectionString)) + return _convertTypes(conn.Execute(_getFieldTypeSql(), new { table = TableName(tableName), schema = SchemaName(tableName) })); + } + + protected Dictionary _convertTypes(ICollection> sysobjectsInfos) + { + return sysobjectsInfos.ToDictionary( + row => row["COLUMN_NAME"].ToString(), + row => _determineType(row)); + } + + protected SqlColumnType _determineType(Dictionary sysobjectInfo) + { + // Fun fact - for longtext fields, MySql returns a ludicrously large value here. + // It's so big, it overflows integer, making an exception + int? length = null; + if(sysobjectInfo["CHARACTER_MAXIMUM_LENGTH"].CanConvertTo()) + length = sysobjectInfo["CHARACTER_MAXIMUM_LENGTH"].ConvertTo(); + // Hack - Sql throws a hissy fit if you try to specify a length beyond 8000, but some field types + // (ie, ntext, return a massive value from the system tables + if (length > 8000) + length = null; + + return new SqlColumnType( + SqlTypeConversion.ParseDbType(sysobjectInfo["DATA_TYPE"].ConvertTo()), + sysobjectInfo["IS_NULLABLE"].ConvertTo(), + length, + sysobjectInfo["NUMERIC_PRECISION"].ConvertTo(), + sysobjectInfo["NUMERIC_SCALE"].ConvertTo()); + } + + public bool TableExists(string tablename) + { + return _tableExists(tablename); + } + + protected bool _tableExistsFromDB(string tablename) + { + using (var conn = _getConnection(_connectionString)) + { + var table = TableName(tablename); + var schema = SchemaName(tablename); + var sql = _getTableExistsSql(schema, table); + var paras = new { table = table, schema = schema }; + return conn.ExecuteSingleValueOr(false, sql, paras); + } + } + + public virtual bool SchemaExists(string schema) + { + return _schemaExists(schema); + } + + protected bool _schemaExistsFromDb(string schema) + { + var paras = new { schema = schema }; + var sql = _getSchemaExistsSql(); + using (var conn = _getConnection(_connectionString)) + { + return conn.ExecuteSingleValueOr(false, sql, paras); + } + } + + public string TableName(string tablename) + { + return NameQualifier.TableName(tablename); + } + + public virtual string SchemaName(string tablename) + { + var res = NameQualifier.SchemaName(tablename); + if (res == "") + res = "dbo"; // If we don't specify a schema, use dbo + + return res; + } + } +} diff --git a/src/Lasy/Sql2000Analyzer.cs b/src/Lasy/Sql2000Analyzer.cs new file mode 100644 index 0000000..2c1200c --- /dev/null +++ b/src/Lasy/Sql2000Analyzer.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Lasy +{ + public class Sql2000Analyzer : SqlAnalyzer + { + public Sql2000Analyzer(string connectionString, TimeSpan cacheDuration = default(TimeSpan)) + : base(connectionString, new Sql2000NameQualifier(), cacheDuration) + { } + + protected internal override string _getPrimaryKeySql() + { + return @"SELECT + isc.COLUMN_NAME as [Name] + FROM + sysobjects tbl + inner join syscolumns c + on tbl.id = c.id + inner join information_schema.columns isc + on isc.column_name = c.name and isc.table_name = tbl.name + left outer join information_schema.key_column_usage k + on k.table_name = tbl.name and objectproperty(object_id(constraint_name), 'IsPrimaryKey') = 1 + and k.column_name = c.name + WHERE + tbl.xtype = 'U' + and tbl.name = @table + AND objectproperty(object_id(constraint_name), 'IsPrimaryKey') = 1 + order by isc.ORDINAL_POSITION"; + } + + protected internal override string _getAutonumberKeySql() + { + return @"SELECT + isc.COLUMN_NAME as [Name] + FROM + sysobjects tbl + inner join syscolumns c + on tbl.id = c.id + inner join information_schema.columns isc + on isc.column_name = c.name and isc.table_name = tbl.name + WHERE + tbl.xtype = 'U' + and tbl.name = @table + AND c.status & 0x80 = 0x80 + order by isc.ORDINAL_POSITION"; + } + + protected internal override string _getTableExistsSql(string schema, string table) + { + return @"SELECT 1 FROM sysobjects tbl + WHERE tbl.xtype = 'U' and tbl.name = @table"; + } + + protected internal override string _getSchemaExistsSql() + { + // Do nothing - SQL 2000 doesn't support schemas + // The only schema is dbo + return "select @schema = 'dbo'"; + } + + protected internal override string _getFieldTypeSql() + { + return @"SELECT + isc.* + FROM + sysobjects tbl + inner join syscolumns c + on tbl.id = c.id + inner join information_schema.columns isc + on isc.column_name = c.name and isc.table_name = tbl.name + left outer join information_schema.key_column_usage k + on k.table_name = tbl.name and objectproperty(object_id(constraint_name), 'IsPrimaryKey') = 1 + and k.column_name = c.name + WHERE + tbl.xtype in ('U','V') + and tbl.name = @table + order by isc.ORDINAL_POSITION"; + } + } +} diff --git a/src/IReadWrite.cs b/src/Lasy/Sql2000NameQualifier.cs similarity index 60% rename from src/IReadWrite.cs rename to src/Lasy/Sql2000NameQualifier.cs index be62e32..6aadc5a 100644 --- a/src/IReadWrite.cs +++ b/src/Lasy/Sql2000NameQualifier.cs @@ -5,7 +5,8 @@ namespace Lasy { - public interface IReadWrite : IReadable, IWriteable + public class Sql2000NameQualifier : SqlNameQualifier { + } } diff --git a/src/Lasy/SqlColumnType.cs b/src/Lasy/SqlColumnType.cs new file mode 100644 index 0000000..8f8e7cc --- /dev/null +++ b/src/Lasy/SqlColumnType.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Data; +using Nvelope; + +namespace Lasy +{ + /// + /// Represents the type of a sql column + /// + public class SqlColumnType + { + public SqlColumnType(SqlDbType type, bool isNullable = false, int? length = null, int? precision = null, int? scale = null) + { + SqlType = type; + IsNullable = isNullable; + Length = length; + Precision = precision; + Scale = scale; + } + + public SqlDbType SqlType; + public DbType DbType + { + get + { + return SqlTypeConversion.GetDbType(SqlType); + } + } + public bool IsNullable; + public int? Length; + public int? Precision; + public int? Scale; + + public override string ToString() + { + var lengthStr = Length.HasValue ? "(" + Length.Value + ")" : ""; + + // Hack - special case: + // n?varchar(max) columns are represented as having length -1 in the system + // tables. Therefore, if we've got something like that, return the appropriate value + if (SqlType.In(SqlDbType.NVarChar, SqlDbType.VarChar) && Length == -1) + lengthStr = "(max)"; + + // Only decimal types have precisions, so don't print it, even if it's + // set for other types + var precisionStr = ""; + if(SqlType == SqlDbType.Decimal && Precision.HasValue) + precisionStr = "(" + Precision.Value + + (Scale.HasValue ? "," + Scale.Value : "") + ")"; + + return SqlType.ToString().ToLower() + + lengthStr + precisionStr + + " " + (IsNullable ? "NULL" : "NOT NULL"); + } + } +} diff --git a/src/Lasy/SqlDB.cs b/src/Lasy/SqlDB.cs new file mode 100644 index 0000000..cd82b9a --- /dev/null +++ b/src/Lasy/SqlDB.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Nvelope; +using System.Data; + +namespace Lasy +{ + /// + /// Provides an implementation of ITransactable for MS SQL Server + /// + public class SqlDB : AbstractSqlReadWrite, ITransactable + { + public SqlDB(string connectionString, SqlAnalyzer analyzer, bool strictTables = true) + : base(connectionString, analyzer, strictTables) + { } + + protected internal virtual IDbConnection _getConnection() + { + return new System.Data.SqlClient.SqlConnection(ConnectionString); + } + + protected override IEnumerable> sqlRead(string sql, Dictionary values = null) + { + if (values == null) + values = new Dictionary(); + + using (var conn = _getConnection()) + { + return conn.Execute(sql, values); + } + } + + protected override int? sqlInsert(string sql, Dictionary values = null) + { + if (values == null) + values = new Dictionary(); + + using (var conn = _getConnection()) + { + return conn.ExecuteSingleValue(sql, values); + } + } + + protected override void sqlUpdate(string sql, Dictionary values = null) + { + if (values == null) + values = new Dictionary(); + + using (var conn = _getConnection()) + { + conn.Execute(sql, values); + } + } + + public virtual ITransaction BeginTransaction() + { + return new SqlDBTransaction(this); + } + } +} diff --git a/src/Lasy/SqlDBTransaction.cs b/src/Lasy/SqlDBTransaction.cs new file mode 100644 index 0000000..25b0beb --- /dev/null +++ b/src/Lasy/SqlDBTransaction.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Data.SqlClient; +using System.Data; + +namespace Lasy +{ + public class SqlDBTransaction : AbstractSqlReadWrite, ITransaction + { + protected IDbConnection _conn; + protected IDbTransaction _transaction; + + public SqlDBTransaction(SqlDB db) + : base(db.ConnectionString, db.SqlAnalyzer, db.StrictTables) + { + _conn = db._getConnection(); + _conn.Open(); + _transaction = _conn.BeginTransaction(); + } + + public void Commit() + { + _transaction.Commit(); + } + + public void Rollback() + { + _transaction.Rollback(); + } + + protected IDbCommand _getCommand(string sql) + { + var command = _conn.CreateCommand(); + command.CommandText = sql; + command.Transaction = _transaction; + return command; + } + + protected override IEnumerable> sqlRead(string sql, Dictionary values = null) + { + if (values == null) + values = new Dictionary(); + + var command = _getCommand(sql); + return command.Execute(sql, values); + } + + protected override int? sqlInsert(string sql, Dictionary values = null) + { + if (values == null) + values = new Dictionary(); + + var command = _getCommand(sql); + return command.ExecuteSingleValue(sql, values); + } + + protected override void sqlUpdate(string sql, Dictionary values = null) + { + if (values == null) + values = new Dictionary(); + + var command = _getCommand(sql); + command.Execute(sql, values); + + } + + public void Dispose() + { + _conn.Dispose(); + } + } +} diff --git a/src/SqlExtensions.cs b/src/Lasy/SqlExtensions.cs similarity index 77% rename from src/SqlExtensions.cs rename to src/Lasy/SqlExtensions.cs index d335feb..d64dabc 100644 --- a/src/SqlExtensions.cs +++ b/src/Lasy/SqlExtensions.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Data; -using System.Data.SqlClient; using System.Linq; +using Nvelope; using Nvelope.Data; using Nvelope.Reflection; @@ -19,10 +19,13 @@ public static class SqlExtensions /// /// /// - public static void AddParameter(this SqlCommand comm, string name, object value) + public static void AddParameter(this IDbCommand comm, string name, object value) { var realizedValue = value.Realize(); - var para = new SqlParameter(name, SqlTypeConversion.InferSqlType(realizedValue)); + var sqlType = SqlTypeConversion.GetSqlType(realizedValue); + var para = comm.CreateParameter(); + para.ParameterName = name; + para.DbType = sqlType.DbType; para.Value = SqlTypeConversion.ConvertToSqlValue(realizedValue); comm.Parameters.Add(para); } @@ -34,7 +37,7 @@ public static void AddParameter(this SqlCommand comm, string name, object value) /// Func{object} - this will call Realize on the values /// /// - public static void AddParameters(this SqlCommand comm, Dictionary paras) + public static void AddParameters(this IDbCommand comm, Dictionary paras) { foreach (var kv in paras) comm.AddParameter(kv.Key, kv.Value); @@ -46,12 +49,12 @@ public static void AddParameters(this SqlCommand comm, Dictionary /// /// - public static ICollection> Execute(this SqlConnection conn, string sql) + public static ICollection> Execute(this IDbConnection conn, string sql) { return Execute(conn, sql, new Dictionary()); } - public static ICollection> Execute(this SqlCommand comm, string sql) + public static ICollection> Execute(this IDbCommand comm, string sql) { return Execute(comm, sql, new Dictionary()); } @@ -64,12 +67,12 @@ public static ICollection> Execute(this SqlCommand co /// /// /// - public static IEnumerable Execute(this SqlConnection conn, string sql) where T : class, new() + public static IEnumerable Execute(this IDbConnection conn, string sql) where T : class, new() { return Execute(conn, sql, new Dictionary()); } - public static IEnumerable Execute(this SqlCommand comm, string sql) where T : class, new() + public static IEnumerable Execute(this IDbCommand comm, string sql) where T : class, new() { return Execute(comm, sql, new Dictionary()); } @@ -83,7 +86,7 @@ public static ICollection> Execute(this SqlCommand co /// /// /// - public static ICollection> Execute(this SqlConnection conn, string sql, + public static ICollection> Execute(this IDbConnection conn, string sql, Dictionary paras) { ICollection> res = null; @@ -91,7 +94,7 @@ public static ICollection> Execute(this SqlConnection return res; } - public static ICollection> Execute(this SqlCommand comm, string sql, + public static ICollection> Execute(this IDbCommand comm, string sql, Dictionary paras) { ICollection> res = null; @@ -111,7 +114,7 @@ public static ICollection> Execute(this SqlCommand co /// /// /// - public static IEnumerable Execute(this SqlConnection conn, string sql, + public static IEnumerable Execute(this IDbConnection conn, string sql, Dictionary paras) where T: class, new() { IEnumerable res = null; @@ -119,7 +122,7 @@ public static IEnumerable Execute(this SqlConnection conn, string sql, return res; } - public static IEnumerable Execute(this SqlCommand comm, string sql, + public static IEnumerable Execute(this IDbCommand comm, string sql, Dictionary paras) where T : class, new() { IEnumerable res = null; @@ -135,13 +138,13 @@ public static IEnumerable Execute(this SqlCommand comm, string sql, /// Converted into a dictionary of field-value pairs, which are /// are used as the parameters of the query /// - public static ICollection> Execute(this SqlConnection conn, string sql, object parameterObject) + public static ICollection> Execute(this IDbConnection conn, string sql, object parameterObject) { var dict = parameterObject as Dictionary; return Execute(conn, sql, dict ?? parameterObject._AsDictionary()); } - public static ICollection> Execute(this SqlCommand comm, string sql, object parameterObject) + public static ICollection> Execute(this IDbCommand comm, string sql, object parameterObject) { var dict = parameterObject as Dictionary; return Execute(comm, sql, dict ?? parameterObject._AsDictionary()); @@ -157,12 +160,12 @@ public static ICollection> Execute(this SqlCommand co /// Converted into a dictionary of field-value pairs, which are /// are used as the parameters of the query /// - public static IEnumerable Execute(this SqlConnection conn, string sql, object parameterObject) where T: class, new() + public static IEnumerable Execute(this IDbConnection conn, string sql, object parameterObject) where T: class, new() { var dict = parameterObject as Dictionary; return Execute(conn, sql, dict ?? parameterObject._AsDictionary()); } - public static IEnumerable Execute(this SqlCommand comm, string sql, object parameterObject) where T : class, new() + public static IEnumerable Execute(this IDbCommand comm, string sql, object parameterObject) where T : class, new() { var dict = parameterObject as Dictionary; return Execute(comm, sql, dict ?? parameterObject._AsDictionary()); @@ -176,12 +179,12 @@ public static ICollection> Execute(this SqlCommand co /// /// /// - public static T ExecuteSingleValue(this SqlConnection conn, string sql) + public static T ExecuteSingleValue(this IDbConnection conn, string sql) { return ExecuteSingleValue(conn, sql, new Dictionary()); } - public static T ExecuteSingleValue(this SqlCommand comm, string sql) + public static T ExecuteSingleValue(this IDbCommand comm, string sql) { return ExecuteSingleValue(comm, sql, new Dictionary()); } @@ -194,16 +197,34 @@ public static T ExecuteSingleValue(this SqlCommand comm, string sql) /// /// /// - public static T ExecuteSingleValue(this SqlConnection conn, string sql, object parameterObject) + public static T ExecuteSingleValue(this IDbConnection conn, string sql, object parameterObject) { - var dict = parameterObject as Dictionary; - return ExecuteSingleValue(conn, sql, dict ?? parameterObject._AsDictionary()); + return ExecuteSingleValue(conn, sql, parameterObject._AsDictionary()); } - public static T ExecuteSingleValue(this SqlCommand comm, string sql, object parameterObject) + public static T ExecuteSingleValue(this IDbCommand comm, string sql, object parameterObject) { - var dict = parameterObject as Dictionary; - return ExecuteSingleValue(comm, sql, dict ?? parameterObject._AsDictionary()); + return ExecuteSingleValue(comm, sql, parameterObject._AsDictionary()); + } + + /// + /// Executes the query and returns the single value returned. If no value is returned from the db, + /// return defaultVal + /// + /// + /// + /// + /// + /// + /// + public static T ExecuteSingleValueOr(this IDbConnection conn, T defaultVal, string sql, object parameterObject) + { + return ExecuteSingleValueOr(conn, defaultVal, sql, parameterObject._AsDictionary()); + } + + public static T ExecuteSingleValueOr(this IDbConnection conn, T defaultVal, string sql, Dictionary parameters) + { + return ExecuteSingleColumn(conn, sql, parameters).SingleOr(defaultVal); } /// @@ -214,12 +235,12 @@ public static T ExecuteSingleValue(this SqlCommand comm, string sql, object p /// /// /// - public static T ExecuteSingleValue(this SqlConnection conn, string sql, Dictionary parameters) + public static T ExecuteSingleValue(this IDbConnection conn, string sql, Dictionary parameters) { return ExecuteSingleColumn(conn, sql, parameters).Single(); } - public static T ExecuteSingleValue(this SqlCommand comm, string sql, Dictionary parameters) + public static T ExecuteSingleValue(this IDbCommand comm, string sql, Dictionary parameters) { return ExecuteSingleColumn(comm, sql, parameters).Single(); } @@ -231,12 +252,12 @@ public static T ExecuteSingleValue(this SqlCommand comm, string sql, Dictiona /// /// /// - public static ICollection ExecuteSingleColumn(this SqlConnection conn, string sql) + public static ICollection ExecuteSingleColumn(this IDbConnection conn, string sql) { return ExecuteSingleColumn(conn, sql, new Dictionary()); } - public static ICollection ExecuteSingleColumn(this SqlCommand comm, string sql) + public static ICollection ExecuteSingleColumn(this IDbCommand comm, string sql) { return ExecuteSingleColumn(comm, sql, new Dictionary()); } @@ -249,13 +270,13 @@ public static ICollection ExecuteSingleColumn(this SqlCommand comm, string /// /// /// - public static ICollection ExecuteSingleColumn(this SqlConnection conn, string sql, object parameterObject) + public static ICollection ExecuteSingleColumn(this IDbConnection conn, string sql, object parameterObject) { var dict = parameterObject as Dictionary; return ExecuteSingleColumn(conn, sql, dict ?? parameterObject._AsDictionary()); } - public static ICollection ExecuteSingleColumn(this SqlCommand comm, string sql, object parameterObject) + public static ICollection ExecuteSingleColumn(this IDbCommand comm, string sql, object parameterObject) { var dict = parameterObject as Dictionary; return ExecuteSingleColumn(comm, sql, dict ?? parameterObject._AsDictionary()); @@ -269,14 +290,14 @@ public static ICollection ExecuteSingleColumn(this SqlCommand comm, string /// /// /// - public static ICollection ExecuteSingleColumn(this SqlConnection conn, string sql, Dictionary parameters) + public static ICollection ExecuteSingleColumn(this IDbConnection conn, string sql, Dictionary parameters) { var data = new List(); Execute(conn, sql, parameters, reader => data = reader.SingleColumn().ToList()); return new ReadOnlyCollection(data); } - public static ICollection ExecuteSingleColumn(this SqlCommand comm, string sql, Dictionary parameters) + public static ICollection ExecuteSingleColumn(this IDbCommand comm, string sql, Dictionary parameters) { var data = new List(); Execute(comm, sql, parameters, reader => data = reader.SingleColumn().ToList()); @@ -290,19 +311,20 @@ public static ICollection ExecuteSingleColumn(this SqlCommand comm, string /// /// /// - public static void Execute(this SqlConnection conn, string sql, + public static void Execute(this IDbConnection conn, string sql, Dictionary paras, Action callback) { if (conn.State == ConnectionState.Closed) conn.Open(); - using (var comm = new SqlCommand(sql, conn)) + using (var comm = conn.CreateCommand()) { + comm.CommandText = sql; comm.Execute(sql, paras, callback); } } - public static void Execute(this SqlCommand comm, string sql, + public static void Execute(this IDbCommand comm, string sql, Dictionary paras, Action callback) { comm.CommandType = CommandType.Text; @@ -320,12 +342,12 @@ public static void Execute(this SqlCommand comm, string sql, /// /// /// - public static void Execute(this SqlConnection conn, string sql, Action callback) + public static void Execute(this IDbConnection conn, string sql, Action callback) { Execute(conn, sql, new Dictionary(), callback); } - public static void Execute(this SqlCommand comm, string sql, Action callback) + public static void Execute(this IDbCommand comm, string sql, Action callback) { Execute(comm, sql, new Dictionary(), callback); } @@ -337,13 +359,13 @@ public static void Execute(this SqlCommand comm, string sql, Action /// /// /// - public static void Execute(this SqlConnection conn, string sql, object parameterObject, Action callback) + public static void Execute(this IDbConnection conn, string sql, object parameterObject, Action callback) { var dict = parameterObject as Dictionary; Execute(conn, sql, dict ?? parameterObject._AsDictionary(), callback); } - public static void Execute(this SqlCommand comm, string sql, object parameterObject, Action callback) + public static void Execute(this IDbCommand comm, string sql, object parameterObject, Action callback) { var dict = parameterObject as Dictionary; Execute(comm, sql, dict ?? parameterObject._AsDictionary(), callback); diff --git a/src/Lasy/SqlModifier.cs b/src/Lasy/SqlModifier.cs new file mode 100644 index 0000000..7445094 --- /dev/null +++ b/src/Lasy/SqlModifier.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Nvelope; +using Nvelope.Reflection; +using System.Reactive.Linq; +using System.Reactive; +using System.Data; + +namespace Lasy +{ + public class SqlModifier : IDBModifier + { + public SqlModifier(string connectionString, SqlAnalyzer analyzer, ITypedDBAnalyzer taxonomy = null) + { + SqlAnalyzer = analyzer; + _connectionString = connectionString; + Taxonomy = taxonomy; + } + + protected string _connectionString; + public ITypedDBAnalyzer Taxonomy { get; set; } + public SqlAnalyzer SqlAnalyzer { get; protected set; } + public IDBAnalyzer Analyzer { get { return SqlAnalyzer; } } + + protected internal virtual IDbConnection _getConnection(string connectionString) + { + return new System.Data.SqlClient.SqlConnection(connectionString); + } + + /// + /// + /// + /// + /// + /// A mapping of the fieldname to .NET type of the fields + /// + public virtual string _getCreateTableSql(string schema, string table, Dictionary fieldTypes) + { + // Strip off the primary key if it was supplied in fields - we'll make it ourselves + var datafields = fieldTypes.Except(table + "Id"); + var fieldList = _fieldDefinitions(datafields); + + var sql = String.Format(@"CREATE TABLE [{0}].[{1}] + ( + {1}Id int NOT NULL IDENTITY (1,1) PRIMARY KEY, + {2} + ) ON [PRIMARY]", + schema, table, fieldList); + + return sql; + } + + public virtual string _getCreateSchemaSql(string schema) + { + return string.Format("CREATE SCHEMA [{0}] AUTHORIZATION [dbo]", schema); + } + + public virtual string _getDropSchemaSql(string schema) + { + return string.Format("drop schema [{0}]", schema); + } + + public virtual string _getDropTableSql(string schema, string table) + { + return string.Format("drop table [{0}].[{1}]", schema, table); + } + + public virtual string _getDropViewSql(string schema, string view) + { + return string.Format("drop view [{0}].[{1}]", schema, view); + } + + public void CreateTable(string tablename, Dictionary fieldTypes) + { + var table = SqlAnalyzer.TableName(tablename); + var schema = SqlAnalyzer.SchemaName(tablename); + var sql = _getCreateTableSql(schema, table, fieldTypes); + var paras = new { table = table, schema = schema }; + + if (!SqlAnalyzer.SchemaExists(schema)) + CreateSchema(schema); + + using (var conn = _getConnection(_connectionString)) + conn.Execute(sql, paras); + + SqlAnalyzer.InvalidateTableCache(tablename); + } + + public void DropTable(string tablename) + { + var table = SqlAnalyzer.TableName(tablename); + var schema = SqlAnalyzer.SchemaName(tablename); + + using (var conn = _getConnection(_connectionString)) + conn.Execute(_getDropTableSql(schema, table)); + + SqlAnalyzer.InvalidateTableCache(tablename); + } + + public void DropView(string viewname) + { + var view = SqlAnalyzer.TableName(viewname); + var schema = SqlAnalyzer.SchemaName(viewname); + + using (var conn = _getConnection(_connectionString)) + conn.Execute(_getDropViewSql(schema, view)); + + SqlAnalyzer.InvalidateTableCache(viewname); + } + + public void DropSchema(string schema) + { + using (var conn = _getConnection(_connectionString)) + conn.Execute(_getDropSchemaSql(schema)); + SqlAnalyzer.InvalidateSchemaCache(schema); + } + + public void CreateSchema(string schema) + { + var sql = _getCreateSchemaSql(schema); + using (var conn = _getConnection(_connectionString)) + { + conn.Execute(sql); + } + SqlAnalyzer.InvalidateSchemaCache(schema); + } + + protected string _fieldDefinitions(Dictionary fieldTypes) + { + var res = fieldTypes.Select(kv => kv.Key + " " + kv.Value.ToString()).Join(", "); + return res; + } + + public void KillSchema(string schema) + { + if (SqlAnalyzer.SchemaExists(schema)) + DropSchema(schema); + } + + public void EnsureSchema(string schema) + { + if (!SqlAnalyzer.SchemaExists(schema)) + CreateSchema(schema); + } + + public void KillView(string viewname) + { + if (SqlAnalyzer.TableExists(viewname)) + DropView(viewname); + } + } +} diff --git a/src/Lasy/SqlNameQualifier.cs b/src/Lasy/SqlNameQualifier.cs new file mode 100644 index 0000000..7ee8677 --- /dev/null +++ b/src/Lasy/SqlNameQualifier.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Nvelope; + +namespace Lasy +{ + public class SqlNameQualifier : INameQualifier + { + public virtual string TableName(string tablename) + { + var res = tablename.Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries).Last().ChopEnd("]").ChopStart("["); + return res; + } + + public virtual string SchemaName(string tablename) + { + var parts = tablename.Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.ChopEnd("]").ChopStart("[")); + if (parts.Count() > 1) + return parts.First(); + else + return ""; + } + + public virtual bool SupportsSchemas + { + get { return true; } + } + } +} diff --git a/src/Lasy/SqlTypeAttribute.cs b/src/Lasy/SqlTypeAttribute.cs new file mode 100644 index 0000000..83106c9 --- /dev/null +++ b/src/Lasy/SqlTypeAttribute.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Data; + +namespace Lasy +{ + /// + /// Used to decorate the fields of an object in order to hint to Lasy what + /// SQL type it should use if creating tables + /// + public class SqlTypeAttribute : Attribute + { + //public SqlTypeAttribute(bool? isNullable = null, + // int? precision = null, + // int? scale = null, + // int? length = null, + // SqlDbType? type = null) + //{ + // IsNullable = isNullable; + // Precision = precision; + // Scale = scale; + // Length = length; + // Type = type; + //} + + public SqlTypeAttribute(SqlDbType? type, bool? isNullable, int? length, int? precision, int? scale) + { + IsNullable = isNullable; + Precision = precision; + Scale = scale; + Length = length; + Type = type; + } + + public SqlTypeAttribute() + : this(null, null, null, null, null) + { } + + public SqlTypeAttribute(int length) + : this(null, null, length, null, null) + { } + + public SqlTypeAttribute(bool isNullable, int length) + : this(null, isNullable, length, null, null) + { } + + + public bool? IsNullable; + public int? Precision; + public int? Scale; + public int? Length; + public SqlDbType? Type; + } +} diff --git a/src/Lasy/SqlTypeConversion.cs b/src/Lasy/SqlTypeConversion.cs new file mode 100644 index 0000000..ff44f74 --- /dev/null +++ b/src/Lasy/SqlTypeConversion.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Data; +using System.Xml; +using Microsoft.SqlServer.Server; +using Nvelope; +using Nvelope.Reflection; +using System.Reflection; + +namespace Lasy +{ + /// + /// Conversion to and from SQL data types + /// + public class SqlTypeConversion + { + static SqlTypeConversion() + { + _toSqlMappings.Add(typeof(bool?), new SqlColumnType(SqlDbType.Bit, true)); + _toSqlMappings.Add(typeof(bool), new SqlColumnType(SqlDbType.Bit)); + _toSqlMappings.Add(typeof(char?), new SqlColumnType(SqlDbType.Char, true, length: 1)); + _toSqlMappings.Add(typeof(char), new SqlColumnType(SqlDbType.Char, false, length: 1)); + _toSqlMappings.Add(typeof(int?), new SqlColumnType(SqlDbType.Int, true)); + _toSqlMappings.Add(typeof(int), new SqlColumnType(SqlDbType.Int)); + _toSqlMappings.Add(typeof(Nullable), new SqlColumnType(SqlDbType.BigInt, true)); + _toSqlMappings.Add(typeof(Int64), new SqlColumnType(SqlDbType.BigInt)); + _toSqlMappings.Add(typeof(float?), new SqlColumnType(SqlDbType.Real, true)); + _toSqlMappings.Add(typeof(float), new SqlColumnType(SqlDbType.Real)); + _toSqlMappings.Add(typeof(double?), new SqlColumnType(SqlDbType.Float, true)); + _toSqlMappings.Add(typeof(double), new SqlColumnType(SqlDbType.Float)); + _toSqlMappings.Add(typeof(decimal?), new SqlColumnType(SqlDbType.Decimal, true, precision: 36, scale: 12)); + _toSqlMappings.Add(typeof(decimal), new SqlColumnType(SqlDbType.Decimal, precision: 36, scale: 12)); + _toSqlMappings.Add(typeof(DateTime?), new SqlColumnType(SqlDbType.DateTime, true)); + _toSqlMappings.Add(typeof(DateTime), new SqlColumnType(SqlDbType.DateTime)); + _toSqlMappings.Add(typeof(string), new SqlColumnType(SqlDbType.NVarChar, true, length: 100)); + _toSqlMappings.Add(typeof(XmlDocument), new SqlColumnType(SqlDbType.Xml, true)); + _toSqlMappings.Add(typeof(Guid?), new SqlColumnType(SqlDbType.UniqueIdentifier, true)); + _toSqlMappings.Add(typeof(Guid), new SqlColumnType(SqlDbType.UniqueIdentifier)); + _toSqlMappings.Add(typeof(Byte), new SqlColumnType(SqlDbType.TinyInt)); + // Note - this isn't ideal - SByte is signed 8-bit, while SmallInt is signed 16-bit + // I don't think there's an exact match + _toSqlMappings.Add(typeof(SByte), new SqlColumnType(SqlDbType.SmallInt)); + + // _toCMappings are just the inverses of the _toSqlMappings, plus a few overloads + _toCMappings = _toSqlMappings.Invert(); + _toCMappings.Add(new SqlColumnType(SqlDbType.NVarChar), typeof(string)); + _toCMappings.Add(new SqlColumnType(SqlDbType.VarChar, true), typeof(string)); + _toCMappings.Add(new SqlColumnType(SqlDbType.VarChar), typeof(string)); + var charTypes = _toCMappings.WhereKeys(k => k.SqlType == SqlDbType.Char).Keys; + foreach (var charType in charTypes) + _toCMappings.Remove(charType); + _toCMappings.Add(new SqlColumnType(SqlDbType.Char), typeof(string)); + _toCMappings.Add(new SqlColumnType(SqlDbType.Char, true), typeof(string)); + _toCMappings.Add(new SqlColumnType(SqlDbType.NChar), typeof(string)); + _toCMappings.Add(new SqlColumnType(SqlDbType.Xml), typeof(XmlDocument)); + + + // _toDbMappings contain mappings of SqlDbTypes to DbTypes + // DbTypes are used for database-agnostic mapping, while SqlDbTypes are SQL-server specific + _toDbMappings.Add(SqlDbType.BigInt, DbType.Int64); + _toDbMappings.Add(SqlDbType.Binary, DbType.Binary); + _toDbMappings.Add(SqlDbType.Bit, DbType.Boolean); + _toDbMappings.Add(SqlDbType.Char, DbType.AnsiString); + _toDbMappings.Add(SqlDbType.Date, DbType.Date); + _toDbMappings.Add(SqlDbType.DateTime, DbType.DateTime); + _toDbMappings.Add(SqlDbType.DateTime2, DbType.DateTime2); + _toDbMappings.Add(SqlDbType.DateTimeOffset, DbType.DateTimeOffset); + _toDbMappings.Add(SqlDbType.Decimal, DbType.Decimal); + _toDbMappings.Add(SqlDbType.Float, DbType.Single); + _toDbMappings.Add(SqlDbType.Image, DbType.Binary); + _toDbMappings.Add(SqlDbType.Int, DbType.Int32); + _toDbMappings.Add(SqlDbType.Money, DbType.Currency); + _toDbMappings.Add(SqlDbType.NChar, DbType.String); + + _toDbMappings.Add(SqlDbType.NText, DbType.String); + _toDbMappings.Add(SqlDbType.NVarChar, DbType.String); + _toDbMappings.Add(SqlDbType.Real, DbType.Double); + _toDbMappings.Add(SqlDbType.SmallDateTime, DbType.DateTime); + _toDbMappings.Add(SqlDbType.SmallInt, DbType.Int16); + _toDbMappings.Add(SqlDbType.SmallMoney, DbType.Currency); + //SqlDbType.Structured + _toDbMappings.Add(SqlDbType.Text, DbType.String); + _toDbMappings.Add(SqlDbType.Time, DbType.Time); + // SqlDbType.Timestamp + _toDbMappings.Add(SqlDbType.TinyInt, DbType.Byte); + //SqlDbType.Udt + _toDbMappings.Add(SqlDbType.UniqueIdentifier, DbType.Guid); + _toDbMappings.Add(SqlDbType.VarBinary, DbType.Binary); + _toDbMappings.Add(SqlDbType.VarChar, DbType.String); + //SqlDbType.Variant + _toDbMappings.Add(SqlDbType.Xml, DbType.Xml); + } + + private static Dictionary _toSqlMappings = new Dictionary(); + private static Dictionary _toCMappings = new Dictionary(); + private static Dictionary _toDbMappings = new Dictionary(); + + private const int MAX_STRING_LENGTH = 4000; + + public static SqlColumnType GetSqlType(Type dotNetType) + { + if (dotNetType == null) + return new SqlColumnType(SqlDbType.NVarChar, true, 100); + + // For enumerations, map to ints + if (dotNetType.IsEnum) + return new SqlColumnType(SqlDbType.Int); + + if (!_toSqlMappings.ContainsKey(dotNetType)) + throw new NotImplementedException("Don't know how to map type '" + dotNetType.Name + "' to a sql type"); + + return _toSqlMappings[dotNetType]; + } + + public static SqlColumnType GetSqlType(object val) + { + if (val == null) + return new SqlColumnType(SqlDbType.NVarChar, true, 100); + if (val == DBNull.Value) + return new SqlColumnType(SqlDbType.NVarChar, true, 100); + + var type = val.GetType(); + var res = GetSqlType(type); + var length = GetAppropriateLength(val) ?? res.Length; + var precision = GetAppropriatePrecision(val) ?? res.Precision; + var scale = GetAppropriateScale(val) ?? res.Scale; + + return new SqlColumnType(res.SqlType, res.IsNullable, length, precision, scale); + } + + public static DbType GetDbType(SqlDbType sqlDbType) + { + if (!_toDbMappings.ContainsKey(sqlDbType)) + throw new NotImplementedException("Don't know how to map SqlDbType '" + sqlDbType + "' to a DbType"); + return _toDbMappings[sqlDbType]; + } + + public static int? GetAppropriateLength(object val) + { + // Hack - if this is a string type, we have to figure out how long a column to make + // This is largely guesswork + if (val is string) + { + var strLen = (val as string).Length; + if (strLen > 30) + return MAX_STRING_LENGTH; + else + return 100; + } + + return null; + } + + public static int? GetAppropriateScale(object val) + { + // TODO: Something intelligent here, rather than just assuming 12 is enough + if (val is decimal) + return 12; + + return null; + } + + public static int? GetAppropriatePrecision(object val) + { + // TODO: Something intelligent here, rather than just assuming 36 is enough + if (val is decimal) + return 36; + return null; + } + + /// + /// If we have a null value we need to return DBNull for the Sql query, other types are converted automatically. + /// + public static object ConvertToSqlValue(object obj) + { + if (obj == null) + return DBNull.Value; + return obj; + } + + /// + /// Figure out the SQL type signified by the supplied type name + /// + /// + /// + public static SqlDbType ParseDbType(string sqlTypeName) + { + // Hack because microsoft doesn't correctly take this into consideration + if (sqlTypeName == "numeric") + sqlTypeName = "decimal"; + // MySql has a longtext type, which we'll convert to nvarchar + if (sqlTypeName == "longtext") + sqlTypeName = "nvarchar"; + + return (SqlDbType)Enum.Parse(typeof(SqlDbType), sqlTypeName, true); + } + + /// + /// Convert to the equivalent .NET type + /// + /// + /// + public static Type GetDotNetType(SqlColumnType ct) + { + var mapped = _toCMappings.Where(kv => kv.Key.SqlType == ct.SqlType && kv.Key.IsNullable == ct.IsNullable); + if (!mapped.Any()) + throw new NotImplementedException("Don't know how to convert SqlColumnType '" + ct.Print() + "' to a .Net type"); + if (mapped.Count() > 1) + throw new NotImplementedException("There was more than one mapping to a .Net type for SqlColumnType " + ct.Print()); + + return mapped.First().Value; + } + } + + public static class SqlTypeExtensions + { + public static Dictionary _SqlFieldTypes(this object obj) + { + var type = obj.GetType(); + return _SqlFieldTypes(type); + } + + public static Dictionary _SqlFieldTypes(this Type type) + { + MemberTypes types = MemberTypes.Property | MemberTypes.Field; + BindingFlags bind = BindingFlags.Instance | BindingFlags.Public; + + var props = type.GetMembers(bind).Where(m => types.HasFlag(m.MemberType)); + var res = props.ToDictionary(m => m.Name, _getSqlType); + + return res; + } + + private static SqlColumnType _getSqlType(MemberInfo mi) + { + // See if there's any SqlTypeAttribute on the mi + // If so, pull the info from there. + var att = mi.GetCustomAttributes(typeof(SqlTypeAttribute), true) + .FirstOr(new SqlTypeAttribute()) as SqlTypeAttribute; + + var baseType = SqlTypeConversion.GetSqlType(mi.ReturnType()); + + // For anything that's missing, get the assumed type info + var res = new SqlColumnType( + att.Type ?? baseType.SqlType, + att.IsNullable ?? baseType.IsNullable, + att.Length ?? baseType.Length, + att.Precision ?? baseType.Precision, + att.Scale ?? baseType.Scale); + + return res; + } + } +} diff --git a/src/Lasy/UnreliableDb.cs b/src/Lasy/UnreliableDb.cs new file mode 100644 index 0000000..c222831 --- /dev/null +++ b/src/Lasy/UnreliableDb.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Lasy; +using Nvelope; + +namespace Lasy +{ + /// + /// A db class that we can set up to fail on the next read operation + /// Useful for simulating DB failures in the middle of transactions + /// + public class UnreliableDb : FakeDB + { + public UnreliableDb() + : base() + { + _setup(); + } + + public UnreliableDb(IDBAnalyzer analyzer) + : base(analyzer) + { + _setup(); + } + + protected void _setup() + { + // Create event handlers for insert, update, delete + // that just call our method to see if FailOnNextOp is set + this.OnDelete += (a, b) => _failCheck(); + this.OnInsert += (a, b) => _failCheck(); + this.OnUpdate += (a, b, c) => _failCheck(); + this.OnRead += (a, b) => _failCheck(); + } + + public bool FailOnNextOp = false; + + protected void _failCheck() + { + if (this.FailOnNextOp) + { + this.FailOnNextOp = false; + throw new MockDBFailure(); + } + } + + } +} diff --git a/src/Lasy/packages.config b/src/Lasy/packages.config new file mode 100644 index 0000000..4308ad3 --- /dev/null +++ b/src/Lasy/packages.config @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/LasyExceptions.cs b/src/LasyExceptions.cs deleted file mode 100644 index 3cd56f1..0000000 --- a/src/LasyExceptions.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Nvelope; - -namespace Lasy -{ - public class KeyNotSetException : Exception - { - public KeyNotSetException(string tableName, IEnumerable keys) - : base("Could not save " + tableName + " because the following fields are null: " + keys.Print()) - { } - } - - public class ThisSadlyHappenedException : Exception - { - public ThisSadlyHappenedException(string message) - : base(message) - { } - } -} diff --git a/src/LasyTests/.svn/entries b/src/LasyTests/.svn/entries deleted file mode 100644 index 5234ba6..0000000 --- a/src/LasyTests/.svn/entries +++ /dev/null @@ -1,31 +0,0 @@ -10 - -dir -13695 -svn://webappsrv2.twu.ca/aqueduct/Lasy/LasyTests -svn://webappsrv2.twu.ca/aqueduct - - - -2011-03-01T23:13:19.120418Z -11981 -Jonathan.Babbitt - - - - - - - - - - - - - - -e4d4639b-ae0f-0410-938a-eb28cb8643ba - -Properties -dir - diff --git a/src/LasyTests/Properties/.svn/entries b/src/LasyTests/Properties/.svn/entries deleted file mode 100644 index 3f5796a..0000000 --- a/src/LasyTests/Properties/.svn/entries +++ /dev/null @@ -1,28 +0,0 @@ -10 - -dir -13695 -svn://webappsrv2.twu.ca/aqueduct/Lasy/LasyTests/Properties -svn://webappsrv2.twu.ca/aqueduct - - - -2011-03-01T23:13:19.120418Z -11981 -Jonathan.Babbitt - - - - - - - - - - - - - - -e4d4639b-ae0f-0410-938a-eb28cb8643ba - diff --git a/src/RealDB.cs b/src/RealDB.cs deleted file mode 100644 index 82883e9..0000000 --- a/src/RealDB.cs +++ /dev/null @@ -1,251 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Data.SqlClient; -using Nvelope; - -namespace Lasy -{ - public class RealDB : IWriteable, IReadable, IDisposable, IReadWrite - { - protected IDBAnalyzer _dbAnalyzer; - protected string connectionString; - protected SqlConnection _conn; - - public RealDB(string connection, IDBAnalyzer dbAnalyzer) - { - _dbAnalyzer = dbAnalyzer; - connectionString = connection; - _conn = new SqlConnection(connection); - _conn.Open(); - } - - #region IReadable Members - - public IEnumerable> RawRead(string tableName, Dictionary keys, ITransaction transaction = null) - { - - var sql = "SELECT * FROM " + tableName + " WHERE " + keys.Select(x => x.Key + " = @" + x.Key).Join(" AND "); - - return sqlRead(sql, transaction, keys); - } - - /// Retrieves only specified columns from a table - /// IEnumerable(string) of column names that you want from a table - /// The keys and values of a SQL where clause to let you find specific table rows - /// Optional - you can pass in a Database transaction for this to be attached to - /// An IEnumerable of a Dictionary with each dictionary representing the column names and values associated with them. - public IEnumerable> RawReadCustomFields(string tableName, IEnumerable fields, Dictionary keys, ITransaction transaction = null) - { - - var sql = "SELECT " + fields.Join(", ") + " FROM " + tableName + " WHERE " + keys.Select(x => x.Key + " = @" + x.Key).Join(" AND "); - - return sqlRead(sql, transaction, keys); - } - - public IEnumerable> RawReadAll(string tableName, ITransaction transaction = null) - { - var sql = "SELECT * FROM " + tableName; - - return sqlRead(sql, transaction); - } - - /// - /// Retrieves only specified columns from a table - /// - /// IEnumerable of column names that you want from a table - /// The keys and values of a SQL where clause to let you find specific table rows - /// Optional - you can pass in a Database transaction for this to be attached to - /// An IEnumerable of a Dictionary with each dictionary representing the column names and values associated with them. - public IEnumerable> RawReadAllCustomFields(string tableName, IEnumerable fields, ITransaction transaction = null) - { - - var sql = "SELECT " + fields.Join(", ") + " FROM " + tableName; - - return sqlRead(sql, transaction); - } - - private IEnumerable> sqlRead(string sql, ITransaction transaction, Dictionary values = null) - { - if (values == null) - values = new Dictionary(); - - //If no transaction is passed in we will make our own connection - if (transaction == null) - { - using (var conn = new SqlConnection(connectionString)) - { - return conn.Execute(sql, values); - } - } - //If a transaction is passed in we will use the global connection - else - { - var command = new SqlCommand(sql, _conn, (transaction as RealDBTransaction).UnderlyingTransaction); - - return command.Execute(sql, values); - } - } - - private int? sqlInsert(string sql, ITransaction transaction, Dictionary values = null) - { - if (values == null) - values = new Dictionary(); - - //If no transaction is passed in we will make our own connection - if (transaction == null) - { - using (var conn = new SqlConnection(connectionString)) - { - return conn.ExecuteSingleValue(sql, values); - } - } - //If a transaction is passed in we will use the global connection - else - { - var command = new SqlCommand(sql, _conn, (transaction as RealDBTransaction).UnderlyingTransaction); - - return command.ExecuteSingleValue(sql, values); - } - } - - private void sqlUpdate(string sql, ITransaction transaction, Dictionary values = null) - { - if (values == null) - values = new Dictionary(); - - //If no transaction is passed in we will make our own connection - if (transaction == null) - { - using (var conn = new SqlConnection(connectionString)) - { - conn.Execute(sql, values); - } - } - //If a transaction is passed in we will use the global connection - else - { - var command = new SqlCommand(sql, _conn, (transaction as RealDBTransaction).UnderlyingTransaction); - - command.Execute(sql, values); - } - } - - public IDBAnalyzer Analyzer - { - get { return _dbAnalyzer; } - } - #endregion - - #region IWriteable Members - - /// - /// Does an insert and returns the keys of the row just inserted - /// - /// - /// - /// - /// - public Dictionary Insert(string tableName, Dictionary dataFields, ITransaction transaction = null) - { - //Retrieve the AutoNumbered key name if there is one - var autoNumberKeyName = Analyzer.GetAutoNumberKey(tableName); - - //Retrieve all of the primary keys to be sure they are all set, except we don't want the autonumbered key since the DB will set it - var requiredKeys = Analyzer.GetPrimaryKeys(tableName).Except(autoNumberKeyName); - - //Check that all of the required keys are actually set - var invalid = requiredKeys.Where(key => dataFields[key] == null); - if (invalid.Any()) - throw new KeyNotSetException(tableName, invalid); - - // Keep in mind that GetFields might return an empty list, which means that it doesn't know - var dbFields = Analyzer.GetFields(tableName).Or(dataFields.Keys); - // Take out the autonumber keys, and take out any supplied data fields - // that aren't actually fields in the DB - var fieldNames = dataFields.Keys.Except(autoNumberKeyName) - .Intersect(dbFields); - - var sql = "INSERT INTO " + tableName + " (" + fieldNames.Join(", ") + ") VALUES (" + fieldNames.Select(x => "@" + x).Join(", ") + ")\n"; - sql += "SELECT SCOPE_IDENTITY()"; - - var autoKey = sqlInsert(sql, transaction, dataFields); - - if (autoNumberKeyName != null && autoKey == null) - throw new ThisSadlyHappenedException("The SQL ran beautifully, but you were expecting an autogenerated number and you did not get it"); - - if (autoNumberKeyName != null) - { - var autoKeyDict = new Dictionary() { { autoNumberKeyName, autoKey } }; - dataFields = dataFields.Union(autoKeyDict); - } - - return dataFields.WhereKeys(key => Analyzer.GetPrimaryKeys(tableName).Contains(key)); - } - - public void Delete(string tableName, Dictionary keyFields, ITransaction transaction = null) - { - var sql = "DELETE FROM " + tableName + " WHERE " + keyFields.Select(x => x.Key + " = @" + x.Key).Join(" AND "); - - sqlUpdate(sql, transaction, keyFields); - } - - /// The fields with data to be udpated in the affected records - /// The identifying fields for which records to update - public void Update(string tableName, Dictionary dataFields, Dictionary keyFields, ITransaction transaction = null) - { - var autoKey = _dbAnalyzer.GetAutoNumberKey(tableName); - - var dataFieldNames = dataFields.Keys.Where(key => key != autoKey); - var dbFields = _dbAnalyzer.GetFields(tableName); - // Don't try to set fields that don't exist in the database - if (dbFields.Any()) // If we don't get anything back, that means we don't know what the DB fields are - dataFieldNames = dataFieldNames.Intersect(dbFields); - - var sql = "UPDATE " + tableName + " SET " + dataFieldNames.Select(x => x + " = @data" + x).Join(", ") + "\n"; - sql += "WHERE " + keyFields.Select(x => x.Key + " = @key" + x.Key).Join(" AND "); - - var data = dataFields.SelectKeys(key => "data" + key); - var keys = keyFields.SelectKeys(key => "key" + key); - - sqlUpdate(sql, transaction, data.Union(keys)); - } - - /// Like Update, but less magic. Does not require an autokey prime number. - /// The fields with data to be udpated in the affected records - /// The identifying fields for which records to update - public void RealUpdate(string tableName, Dictionary dataFields, Dictionary keyFields, ITransaction transaction = null) - { - var sql = "UPDATE " + tableName + " SET " + dataFields.Keys.Select(x => x + " = @data" + x).Join(", ") + "\n"; - sql += "WHERE " + keyFields.Select(x => x.Key + " = @key" + x.Key).Join(" AND "); - - var data = dataFields.SelectKeys(key => "data" + key); - var keys = keyFields.SelectKeys(key => "key" + key); - - sqlUpdate(sql, transaction, data.Union(keys)); - } - - /// - /// Get a new SqlTransaction wrapped in an ITransaction - /// - public ITransaction BeginTransaction() - { - var transaction = _conn.BeginTransaction(); - - return new RealDBTransaction(transaction); - } - #endregion - - #region IDisposable Members - - public void Dispose() - { - _conn.Dispose(); - } - - #endregion - - - } -} diff --git a/src/RealDBTransaction.cs b/src/RealDBTransaction.cs deleted file mode 100644 index 819e4ed..0000000 --- a/src/RealDBTransaction.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Data.SqlClient; - -namespace Lasy -{ - public class RealDBTransaction : ITransaction - { - public SqlTransaction UnderlyingTransaction; - - public RealDBTransaction(SqlTransaction transaction) - { - UnderlyingTransaction = transaction; - } - - #region ITransaction Members - - public void Commit() - { - UnderlyingTransaction.Commit(); - } - - public void Rollback() - { - UnderlyingTransaction.Rollback(); - } - - #endregion - } -} diff --git a/src/SQL2000DBAnalyzer.cs b/src/SQL2000DBAnalyzer.cs deleted file mode 100644 index 6ea75ad..0000000 --- a/src/SQL2000DBAnalyzer.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Data.SqlClient; - -namespace Lasy -{ - public class SQL2000DBAnalyzer : SQLAnalyzer - { - public SQL2000DBAnalyzer(string connectionString, TimeSpan cacheDuration = default(TimeSpan)) - : base(connectionString, cacheDuration) - { } - - - protected override string _getPrimaryKeySql() - { - return @"SELECT - isc.COLUMN_NAME as [Name] - FROM - sysobjects tbl - inner join syscolumns c - on tbl.id = c.id - inner join information_schema.columns isc - on isc.column_name = c.name and isc.table_name = tbl.name - left outer join information_schema.key_column_usage k - on k.table_name = tbl.name and objectproperty(object_id(constraint_name), 'IsPrimaryKey') = 1 - and k.column_name = c.name - WHERE - tbl.xtype = 'U' - and tbl.name = @table - AND objectproperty(object_id(constraint_name), 'IsPrimaryKey') = 1 - order by isc.ORDINAL_POSITION"; - } - - protected override string _getAutonumberKeySql() - { - return @"SELECT - isc.COLUMN_NAME as [Name] - FROM - sysobjects tbl - inner join syscolumns c - on tbl.id = c.id - inner join information_schema.columns isc - on isc.column_name = c.name and isc.table_name = tbl.name - WHERE - tbl.xtype = 'U' - and tbl.name = @table - AND c.status & 0x80 = 0x80 - order by isc.ORDINAL_POSITION"; - } - - protected override string _getFieldsSql() - { - return @"SELECT - isc.COLUMN_NAME as [Name] - FROM - sysobjects tbl - inner join syscolumns c - on tbl.id = c.id - inner join information_schema.columns isc - on isc.column_name = c.name and isc.table_name = tbl.name - left outer join information_schema.key_column_usage k - on k.table_name = tbl.name and objectproperty(object_id(constraint_name), 'IsPrimaryKey') = 1 - and k.column_name = c.name - WHERE - tbl.xtype = 'U' - and tbl.name = @table - order by isc.ORDINAL_POSITION"; - } - } -} diff --git a/src/SQL2005DBAnalyzer.cs b/src/SQL2005DBAnalyzer.cs deleted file mode 100644 index 0779845..0000000 --- a/src/SQL2005DBAnalyzer.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Data.SqlClient; -using Nvelope; -using System.Text.RegularExpressions; - -namespace Lasy -{ - public class SQL2005DBAnalyzer : SQLAnalyzer - { - public SQL2005DBAnalyzer(string connectionString, TimeSpan cacheDuration = default(TimeSpan)) - : base(connectionString, cacheDuration) - { } - - protected override string _getPrimaryKeySql() - { - return @"select isc.Column_name - from - sys.columns c inner join sys.tables t on c.object_id = t.object_id - inner join information_schema.columns isc - on schema_id(isc.TABLE_SCHEMA) = t.schema_id and isc.TABLE_NAME = t.name and isc.COLUMN_NAME = c.name - left join INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE cu - on cu.TABLE_SCHEMA = isc.TABLE_SCHEMA and cu.TABLE_NAME = isc.TABLE_NAME and cu.COLUMN_NAME = isc.COLUMN_NAME - left join INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc - on cu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME and tc.CONSTRAINT_TYPE = 'PRIMARY KEY' - where isc.TABLE_NAME = @table and tc.CONSTRAINT_TYPE = 'PRIMARY KEY' - order by ORDINAL_POSITION"; - } - - protected override string _getAutonumberKeySql() - { - return @"select isc.Column_name - from - sys.columns c inner join sys.tables t on c.object_id = t.object_id - inner join information_schema.columns isc - on schema_id(isc.TABLE_SCHEMA) = t.schema_id and isc.TABLE_NAME = t.name and isc.COLUMN_NAME = c.name - left join INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE cu - on cu.TABLE_SCHEMA = isc.TABLE_SCHEMA and cu.TABLE_NAME = isc.TABLE_NAME and cu.COLUMN_NAME = isc.COLUMN_NAME - left join INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc - on cu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME and tc.CONSTRAINT_TYPE = 'PRIMARY KEY' - where isc.TABLE_NAME = @table and is_identity = 1 - order by ORDINAL_POSITION"; - } - - protected override string _getFieldsSql() - { - return @"select - isc.COLUMN_NAME as [Name] - from - sys.columns c inner join sys.tables t on c.object_id = t.object_id - inner join information_schema.columns isc - on schema_id(isc.TABLE_SCHEMA) = t.schema_id and isc.TABLE_NAME = t.name and isc.COLUMN_NAME = c.name - LEFT OUTER JOIN sys.default_constraints def - ON def.parent_object_id = c.object_id AND def.parent_column_id = c.column_id - left join INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE cu - on cu.TABLE_SCHEMA = isc.TABLE_SCHEMA and cu.TABLE_NAME = isc.TABLE_NAME and cu.COLUMN_NAME = isc.COLUMN_NAME - left join INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc - on cu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME and tc.CONSTRAINT_TYPE = 'PRIMARY KEY' - where isc.TABLE_NAME = @table - order by ORDINAL_POSITION"; - } - } -} diff --git a/src/SQLAnalyzer.cs b/src/SQLAnalyzer.cs deleted file mode 100644 index befe09e..0000000 --- a/src/SQLAnalyzer.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Data.SqlClient; -using Nvelope; - -namespace Lasy -{ - public abstract class SQLAnalyzer : IDBAnalyzer - { - public SQLAnalyzer(string connectionString, TimeSpan cacheDuration = default(TimeSpan)) - { - if(cacheDuration == default(TimeSpan)) - cacheDuration = new TimeSpan(0,10,0); - - _connectionString = connectionString; - _getAutonumberKey = new Func(_s_getAutonumberKey).Memoize(cacheDuration); - _getFields = new Func>(_s_getFields).Memoize(cacheDuration); - _getPrimaryKeys = new Func>(_s_getPrimaryKeys).Memoize(cacheDuration); - - } - - protected Func> _getPrimaryKeys; - protected Func _getAutonumberKey; - protected Func> _getFields; - protected string _connectionString; - - protected abstract string _getPrimaryKeySql(); - protected abstract string _getAutonumberKeySql(); - protected abstract string _getFieldsSql(); - - public ICollection GetPrimaryKeys(string tableName) - { - return _getPrimaryKeys(tableName); - } - - private ICollection _s_getPrimaryKeys(string tableName) - { - using (var conn = new SqlConnection(_connectionString)) - { - return conn.ExecuteSingleColumn(_getPrimaryKeySql(), new { table = _stripSchema(tableName) }); - } - } - - public string GetAutoNumberKey(string tableName) - { - return _getAutonumberKey(tableName); - } - - private string _s_getAutonumberKey(string tableName) - { - using (var conn = new SqlConnection(_connectionString)) - { - var res = conn.ExecuteSingleColumn(_getAutonumberKeySql(), new { table = _stripSchema(tableName) }); - return res.FirstOr(null); - } - } - - private string _stripSchema(string tablename) - { - var res = tablename.Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries).Last().ChopEnd("]").ChopStart("["); - return res; - } - - public ICollection GetFields(string tableName) - { - return _getFields(tableName); - } - - protected ICollection _s_getFields(string tableName) - { - using (var conn = new SqlConnection(_connectionString)) - { - return conn.ExecuteSingleColumn(_getFieldsSql(), new { table = _stripSchema(tableName) }); - } - } - } -} diff --git a/src/SqlTypeConversion.cs b/src/SqlTypeConversion.cs deleted file mode 100644 index ce81e4d..0000000 --- a/src/SqlTypeConversion.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Data; -using System.Xml; - -namespace Lasy -{ - /// - /// Conversion to and from SQL data types - /// - public class SqlTypeConversion - { - /// - /// Figure out what the SQL type of the supplied object would be - /// - /// - /// - public static SqlDbType InferSqlType(object obj) - { - if (obj == null) - return SqlDbType.VarChar; - if (obj == DBNull.Value) - return SqlDbType.VarChar; - return Microsoft.SqlServer.Server.SqlMetaData.InferFromValue(obj, "").SqlDbType; - } - - /// - /// If we have a null value we need to return DBNull for the Sql query, other types are converted automatically. - /// - public static object ConvertToSqlValue(object obj) - { - if (obj == null) - return DBNull.Value; - return obj; - } - - /// - /// Figure out the SQL type signified by the supplied type name - /// - /// - /// - public static SqlDbType ParseDbType(string sqlTypeName) - { - return (SqlDbType)Enum.Parse(typeof(SqlDbType), sqlTypeName, true); - } - - /// - /// Convert to the equivalent .NET type - /// - /// - /// - public static Type GetDotNetType(string sqlTypeName) - { - var type = ParseDbType(sqlTypeName); - // Should implement this table when done: http://msdn.microsoft.com/en-us/library/cc716729.aspx - switch (type) - { - case (SqlDbType.Bit): - return typeof(bool); - case (SqlDbType.Char): - case (SqlDbType.NChar): - case (SqlDbType.NText): - case (SqlDbType.NVarChar): - case (SqlDbType.Text): - case (SqlDbType.VarChar): - return typeof(string); - case (SqlDbType.Date): - case (SqlDbType.DateTime): - case (SqlDbType.SmallDateTime): - return typeof(DateTime); - case (SqlDbType.Decimal): - case (SqlDbType.Money): - case (SqlDbType.SmallMoney): - return typeof(decimal); - case (SqlDbType.Float): - return typeof(double); - case (SqlDbType.Int): - return typeof(int); - case (SqlDbType.Real): - return typeof(float); - case (SqlDbType.Xml): - return typeof(XmlDocument); - case (SqlDbType.UniqueIdentifier): - return typeof(Guid); - default: - throw new Exception("Unsupported type: '" + type.ToString() + "'"); - } - } - - - } -} diff --git a/test-dbs/LasyTests.mdf b/test-dbs/LasyTests.mdf new file mode 100644 index 0000000..23876fe Binary files /dev/null and b/test-dbs/LasyTests.mdf differ diff --git a/test/IReadWriteExtensionTests.cs b/test/IReadWriteExtensionTests.cs deleted file mode 100644 index 4bb9ea7..0000000 --- a/test/IReadWriteExtensionTests.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using NUnit.Framework; -using Lasy; -using Nvelope; - -namespace LasyTests -{ - [TestFixture] - public class IReadWriteExtensionTests - { - [Test] - public void Ensure() - { - var db = new FakeDB(); - var data = new { MyTableId = 1, Foo = "bar"}; - // This should do an insert - db.Ensure("MyTable", data); - Assert.AreEqual("(([Foo,bar],[MyTableId,1]))", db.Table("MyTable").Print()); - - var newData = new { MyTableId = 1, Foo = "sums" }; - // This should do an update - db.Ensure("MyTable", newData); - Assert.AreEqual("(([Foo,sums],[MyTableId,1]))", db.Table("MyTable").Print()); - } - } -} diff --git a/test/Lasy.Tests/Config.cs b/test/Lasy.Tests/Config.cs new file mode 100644 index 0000000..0d8f847 --- /dev/null +++ b/test/Lasy.Tests/Config.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LasyTests +{ + public static class Config + { + public const string TestDBConnectionString = "Data Source=.\\SQLEXPRESS; Initial Catalog=LasyTests;Integrated Security=SSPI;"; + public const string TestMySqlConnectionString = "Server=localhost;Database=lasytest;Uid=lasy;Pwd=abc123;"; + } +} diff --git a/test/Lasy.Tests/DictBasedTestObject.cs b/test/Lasy.Tests/DictBasedTestObject.cs new file mode 100644 index 0000000..1be90b9 --- /dev/null +++ b/test/Lasy.Tests/DictBasedTestObject.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Nvelope; + +namespace LasyTests +{ + public class DictBasedTestObject : Dictionary + { + public bool IsSet + { + get { return this.Val("IsSet", false).ConvertTo(); } + set { this["IsSet"] = value; } + } + + public string Name + { + get { return this.Val("Name", "").ConvertTo(); } + set { this["Name"] = value; } + } + } +} diff --git a/test/Lasy.Tests/EventTests.cs b/test/Lasy.Tests/EventTests.cs new file mode 100644 index 0000000..3f77d75 --- /dev/null +++ b/test/Lasy.Tests/EventTests.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NUnit.Framework; +using Lasy; +using Nvelope; +using Nvelope.Reflection; + +namespace LasyTests +{ + public abstract class EventTests + { + public abstract IRWEvented _getDb(); + + [Test] + public void OnInsert() + { + var db = TestEnv.SetupInsert(_getDb()); + // When we do an insert, it should increment the counter + var count = 0; + db.OnInsert += (x, y) => ++count; + + db.Insert(TestEnv.Table, TestEnv.Keys); + + Assert.AreEqual(1, count, + "The operation did not fire the event on the database"); + } + + [Test] + public void OnInsertTransaction() + { + var db = TestEnv.SetupInsert(_getDb()); + // When we do an insert, it should increment the counter + var count = 0; + db.OnInsert += (x, y) => ++count; + + if (!(db is ITransactable)) + return; + + var tdb = db as ITransactable; + + var trans = tdb.BeginTransaction(); + trans.Insert(TestEnv.Table, TestEnv.Keys); + trans.Commit(); + + Assert.AreEqual(1, count, + "The transaction did not fire the event on the database"); + } + + [Test] + public void OnUpdate() + { + var db = TestEnv.SetupUpdate(_getDb()); + var count = 0; + db.OnUpdate += (x, y, z) => ++count; + + db.Update(TestEnv.Table, TestEnv.Keys, TestEnv.UpdatedRow); + + Assert.AreEqual(1, count, + "The operation did not fire the event on the database"); + } + + [Test] + public void OnUpdateTransaction() + { + var db = TestEnv.SetupUpdate(_getDb()); + var count = 0; + db.OnUpdate += (x, y, z) => ++count; + + if (!(db is ITransactable)) + return; + + var tdb = db as ITransactable; + + var trans = tdb.BeginTransaction(); + trans.Update(TestEnv.Table, TestEnv.Keys, TestEnv.UpdatedRow); + trans.Commit(); + + Assert.AreEqual(1, count, + "The transaction did not fire the event on the database"); + } + + [Test] + public void OnDelete() + { + var db = TestEnv.SetupDelete(_getDb()); + var count = 0; + db.OnDelete += (x,y) => ++count; + + db.Delete(TestEnv.Table, TestEnv.Keys); + + Assert.AreEqual(1, count, + "The operation did not fire the event on the database"); + } + + [Test] + public void OnDeleteTransaction() + { + var db = TestEnv.SetupDelete(_getDb()); + var count = 0; + db.OnDelete += (x,y) => ++count; + + if (!(db is ITransactable)) + return; + + var tdb = db as ITransactable; + + var trans = tdb.BeginTransaction(); + trans.Delete(TestEnv.Table, TestEnv.Keys); + trans.Commit(); + + Assert.AreEqual(1, count, + "The transaction did not fire the event on the database"); + } + + [Test] + public void OnRead() + { + var db = TestEnv.SetupUpdate(_getDb()); + var count = 0; + db.OnRead += (x,y) => ++count; + + db.Read(TestEnv.Table, TestEnv.Keys); + + Assert.AreEqual(1, count, + "The operation did not fire the event on the database"); + } + + [Test] + public void OnReadTransaction() + { + var db = TestEnv.SetupUpdate(_getDb()); + var count = 0; + db.OnRead += (x, y) => ++count; + + if (!(db is ITransactable)) + return; + + var tdb = db as ITransactable; + + var trans = tdb.BeginTransaction(); + trans.Read(TestEnv.Table, TestEnv.Keys); + trans.Commit(); + + Assert.AreEqual(1, count, + "The transaction did not fire the event on the database"); + } + } +} diff --git a/test/FakeDBTableTests.cs b/test/Lasy.Tests/FakeDBTableTests.cs similarity index 100% rename from test/FakeDBTableTests.cs rename to test/Lasy.Tests/FakeDBTableTests.cs diff --git a/test/FakeDBTests.cs b/test/Lasy.Tests/FakeDBTests.cs similarity index 56% rename from test/FakeDBTests.cs rename to test/Lasy.Tests/FakeDBTests.cs index 41ce4c2..acf3ef2 100644 --- a/test/FakeDBTests.cs +++ b/test/Lasy.Tests/FakeDBTests.cs @@ -3,12 +3,51 @@ using Nvelope.Reflection; using Lasy; using NUnit.Framework; +using System.Collections.Generic; +using System.Linq; namespace LasyTests { + [TestFixture] + public class FakeDbReadConsistency : ReadConsistencyTests + { + protected override IReadWrite _getDb() + { + return new FakeDB(); + } + } + + [TestFixture] + public class FakeDbTransactionTests : TransactionTests + { + public override ITransactable _getDb() + { + return new FakeDB(); + } + } + + [TestFixture] + public class FakeDBEventTests : EventTests + { + public override IRWEvented _getDb() + { + return new FakeDB(); + } + } + [TestFixture] public class FakeDBTests { + [Test] + public void Instantiate() + { + var db = new FakeDB(); + var data = new { F = 1, B = 2 }; + var key = db.Insert("tbl", data); + Assert.NotNull(key); + Assert.NotNull(db); + } + [Test] public void InsertPK() { @@ -26,7 +65,7 @@ public void InsertPK() public void ReadAllFromNewTable() { var db = new FakeDB(); - Assert.AreEqual("()", db.RawReadAll("foosums").Print()); + Assert.AreEqual("()", db.ReadAll("foosums").Print()); } [Test(Description = "Does it work to Read from a table that we've never mentioned before? - It should return nothing")] @@ -71,6 +110,25 @@ public void DoesntRepeatAutoKeys() db.Insert("P", new {Z = "B"}); Assert.AreEqual("(([PId,1],[Z,A]),([PId,3],[Z,B]))", db.Table("P").Print()); } + + [Test(Description = @"We can't create anonymous objects with properties that are null, but we'd like to be + able to do filters like Read(new {Hanlded = null}). In order to do this, we can use DBNull.Value + instead, but then we have to ensure that the system handles DBNull and null the same")] + public void TreatsNullsAndDbNullsTheSame() + { + var db = new FakeDB(); + var rows = new object[]{ + new { Age = DBNull.Value }, + new Person() { Age = null } + }; + + var ids = rows.Select(r => db.InsertAutoKey("Person", r)).ToList(); + + var fromDb = db.Read("Person", new { Age = DBNull.Value }); + Assert.AreEqual(2, fromDb.Count()); + Assert.AreEqual(fromDb.First()["Age"], fromDb.Second()["Age"]); + } + } } diff --git a/test/FileDBData/.svn/entries b/test/Lasy.Tests/FileDBData/.svn/entries similarity index 100% rename from test/FileDBData/.svn/entries rename to test/Lasy.Tests/FileDBData/.svn/entries diff --git a/test/FileDBData/.svn/text-base/simple.rpt.svn-base b/test/Lasy.Tests/FileDBData/.svn/text-base/simple.rpt.svn-base similarity index 100% rename from test/FileDBData/.svn/text-base/simple.rpt.svn-base rename to test/Lasy.Tests/FileDBData/.svn/text-base/simple.rpt.svn-base diff --git a/test/FileDBData/.svn/text-base/variableWidth.rpt.svn-base b/test/Lasy.Tests/FileDBData/.svn/text-base/variableWidth.rpt.svn-base similarity index 100% rename from test/FileDBData/.svn/text-base/variableWidth.rpt.svn-base rename to test/Lasy.Tests/FileDBData/.svn/text-base/variableWidth.rpt.svn-base diff --git a/test/FileDBData/.svn/text-base/withNull.rpt.svn-base b/test/Lasy.Tests/FileDBData/.svn/text-base/withNull.rpt.svn-base similarity index 100% rename from test/FileDBData/.svn/text-base/withNull.rpt.svn-base rename to test/Lasy.Tests/FileDBData/.svn/text-base/withNull.rpt.svn-base diff --git a/test/FileDBData/simple.rpt b/test/Lasy.Tests/FileDBData/simple.rpt similarity index 100% rename from test/FileDBData/simple.rpt rename to test/Lasy.Tests/FileDBData/simple.rpt diff --git a/test/FileDBData/variableWidth.rpt b/test/Lasy.Tests/FileDBData/variableWidth.rpt similarity index 100% rename from test/FileDBData/variableWidth.rpt rename to test/Lasy.Tests/FileDBData/variableWidth.rpt diff --git a/test/FileDBData/withNull.rpt b/test/Lasy.Tests/FileDBData/withNull.rpt similarity index 100% rename from test/FileDBData/withNull.rpt rename to test/Lasy.Tests/FileDBData/withNull.rpt diff --git a/test/FileDBTests.cs b/test/Lasy.Tests/FileDBTests.cs similarity index 81% rename from test/FileDBTests.cs rename to test/Lasy.Tests/FileDBTests.cs index bfc8fa9..7f5f470 100644 --- a/test/FileDBTests.cs +++ b/test/Lasy.Tests/FileDBTests.cs @@ -23,7 +23,7 @@ public void Setup() [Test] public void ReadsData() { - var vals = _db.RawReadAll("Simple"); + var vals = _db.ReadAll("Simple"); Assert.AreEqual( "(([Deprecated,False],[ID,1],[Name,Foo],[Value,Val]),([Deprecated,True],[ID,2],[Name,Bar],[Value,Bal]))", vals.Print()); @@ -32,7 +32,7 @@ public void ReadsData() [Test] public void VariableWidthColumns() { - var vals = _db.RawReadAll("VariableWidth"); + var vals = _db.ReadAll("VariableWidth"); Assert.AreEqual( "(([Deprecated,False],[ID,1],[Name,Foobar],[Value,Foovalue]),([Deprecated,True],[ID,2],[Name,Bar],[Value,Bal]))", vals.Print()); @@ -41,7 +41,7 @@ public void VariableWidthColumns() [Test] public void ConvertsNulls() { - var vals = _db.RawReadAll("WithNull"); + var vals = _db.ReadAll("WithNull"); Assert.AreEqual( "(([Deprecated,],[ID,1],[Name,Foo],[Value,Val]),([Deprecated,True],[ID,2],[Name,Bar],[Value,]))", vals.Print()); @@ -53,21 +53,21 @@ public void ConvertsNulls() [Test] public void ReadCustomFields() { - var vals = _db.RawReadCustomFields("Simple", "ID".And("Name"), new { ID = 1 }._AsDictionary()); + var vals = _db.Read("Simple", new { ID = 1 }._AsDictionary(), "ID".And("Name")); Assert.AreEqual("(([ID,1],[Name,Foo]))", vals.Print()); } [Test] public void Read() { - var vals = _db.RawRead("Simple", new { ID = 1 }._AsDictionary()); + var vals = _db.Read("Simple", new { ID = 1 }._AsDictionary()); Assert.AreEqual("(([Deprecated,False],[ID,1],[Name,Foo],[Value,Val]))", vals.Print()); } [Test] public void ReadAllCustomFields() { - var vals = _db.RawReadAllCustomFields("Simple", "ID".And("Name")); + var vals = _db.RawRead("Simple", null, "ID".And("Name")); Assert.AreEqual("(([ID,1],[Name,Foo]),([ID,2],[Name,Bar]))", vals.Print()); } @@ -84,7 +84,7 @@ public void InfervertPerformsAdequately() }; var db = new FileDB(""); - Action fn = (str, type) => Assert.AreEqual(type, db.Infervert(str).GetType()); + Action fn = (str, type) => Assert.AreEqual(type, Nvelope.Reading.TypeConversion.Infervert(str).GetType()); var time = fn.Benchmark(testVals.Repeat(5000)); // We should be able to do this in under 200ms (arbitrary number) diff --git a/test/Lasy.Tests/FunctionExtensionTests.cs b/test/Lasy.Tests/FunctionExtensionTests.cs new file mode 100644 index 0000000..ae504b5 --- /dev/null +++ b/test/Lasy.Tests/FunctionExtensionTests.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NUnit.Framework; +using System.Reactive; +using System.Reactive.Linq; +using System.Threading; +using Lasy; + +namespace LasyTests +{ + [TestFixture] + public class FunctionExtensionTests + { + [Test] + public void Memoize() + { + // TODO: Figure out a way to test this without using thread timing, or + // at least reduce the amount of thread timing. It's not necessarily + // reliable, and at least some of this stuff (ie, the cache invalidator) + // doesn't necessarily need to be time-based + + var calls = new List(); + Func fn = i => {calls.Add(i); return i.ToString(); }; + + // Create a sequence of 1s generated every X ms + // This will be the feed we pass into the cache as the cacheinvalidator + var invalids = Observable.Generate( + initialState: 1, + condition: t => true, + iterate: t => 1, + resultSelector: t => 1, + timeSelector: t => TimeSpan.FromMilliseconds(30)); + + var memfn = fn.Memoize(TimeSpan.FromMilliseconds(100), invalids); + // When we call once, we should have made one call to the underlying fn + memfn(1); + Assert.AreEqual(1, calls.Count()); + // If we immediately call again, we should use the cached value + memfn(1); + Assert.AreEqual(1, calls.Count()); + + // Now, let's wait until the cacheinvalidator fires + // This should trigger us to make another call to the underlying fn + Thread.Sleep(40); + memfn(1); + Assert.AreEqual(2, calls.Count()); + // But again, immediately calling it should result in the cached value - no call to the underlying fn + memfn(1); + Assert.AreEqual(2, calls.Count()); + + // Now, wait until the timeout on the cache expires - this should also generate a call + // to the underlying fn + Thread.Sleep(150); + memfn(1); + Assert.AreEqual(3, calls.Count()); + } + } +} diff --git a/test/Lasy.Tests/IReadWriteExtensionTests.cs b/test/Lasy.Tests/IReadWriteExtensionTests.cs new file mode 100644 index 0000000..8ab83a9 --- /dev/null +++ b/test/Lasy.Tests/IReadWriteExtensionTests.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NUnit.Framework; +using Lasy; +using Nvelope; + +namespace LasyTests +{ + [TestFixture] + public class IReadWriteExtensionTests + { + [Test] + public void Ensure() + { + var db = new FakeDB(); + var data = new { MyTableId = 1, Foo = "bar"}; + // This should do an insert + db.Ensure("MyTable", data); + Assert.AreEqual("(([Foo,bar],[MyTableId,1]))", db.Table("MyTable").Print()); + + var newData = new { MyTableId = 1, Foo = "sums" }; + // This should do an update + db.Ensure("MyTable", newData); + Assert.AreEqual("(([Foo,sums],[MyTableId,1]))", db.Table("MyTable").Print()); + } + + [Test(Description="If we've got a data class that inherits from dictionary, " + + " we should be able to use the idiom of db.Read(), even if we have fields that " + + "might be null in the database. It's up to the dict-based class to handle that by " + + " providing default values for the fields. That's part of the appeal of using these " + + " dictionary-based data classes - they allow for incomplete specification")] + public void ReadTCanSetFieldsFromNullForDictionaryBasedTypes() + { + var db = new FakeDB(); + var row = new Dictionary{{"Name", "foosums"}}; + db.Insert("test", row); + + // Previously, the way we did this was to write this as: + // obj = db.ReadAll("test").Select(r => new DictBasedTestObject()._SetFrom(r)).Single(); + var obj = db.Read("test", row).Single(); + Assert.NotNull(obj); + Assert.False(obj.IsSet); + Assert.AreEqual("foosums", obj.Name); + } + + [Test(Description = "If we've got a data class that inherits from dictionary, " + + " we should be able to use the idiom of db.Read(), even if we have fields that " + + "might be null in the database. It's up to the dict-based class to handle that by " + + " providing default values for the fields. That's part of the appeal of using these " + + " dictionary-based data classes - they allow for incomplete specification")] + public void ReadAllTCanSetFieldsFromNullForDictionaryBasedTypes() + { + var db = new FakeDB(); + var row = new Dictionary { { "Name", "foosums" } }; + db.Insert("test", row); + + // Previously, the way we did this was to write this as: + // obj = db.ReadAll("test").Select(r => new DictBasedTestObject()._SetFrom(r)).Single(); + var obj = db.ReadAll("test").Single(); + Assert.NotNull(obj); + Assert.False(obj.IsSet); + Assert.AreEqual("foosums", obj.Name); + } + } +} diff --git a/test/Lasy.Tests/IntegrationTests/MySql.cs b/test/Lasy.Tests/IntegrationTests/MySql.cs new file mode 100644 index 0000000..70efc73 --- /dev/null +++ b/test/Lasy.Tests/IntegrationTests/MySql.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NUnit.Framework; +using Nvelope; +using Lasy; + +namespace LasyTests.IntegrationTests +{ + [TestFixture] + public class MySql + { + [Test] + public void ClassesMd_User_read() + { + var db = ConnectTo.MySql(Config.TestMySqlConnectionString); + var res = db.Read("classesmd_user", null).First(); + Assert.NotNull(res); + } + + [Test] + public void ClassesMd_User_write() + { + var db = ConnectTo.MySql(Config.TestMySqlConnectionString); + var res = db.Read("classesmd_user", null).First(); + var key = res.Only("id"); + db.Update("classesmd_user", res, key); + var updated = db.Read("classesmd_user", key); + } + } +} diff --git a/test/Lasy.Tests/JsonDBTests.cs b/test/Lasy.Tests/JsonDBTests.cs new file mode 100644 index 0000000..d8b1cb9 --- /dev/null +++ b/test/Lasy.Tests/JsonDBTests.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NUnit.Framework; +using Lasy; +using Nvelope.IO; +using Nvelope; +using System.IO; +using Nvelope.Reading; + +namespace LasyTests +{ + public class JsonDBTests + { + protected object _foobar + { + get + { + return new { A = 1, B = "str" }; + } + } + + protected string _file = "test.json"; + + [TestCase("", Result = "foobar\r\n{\"A\":1,\"B\":\"str\",\"foobarId\":1}")] + [TestCase("foobar\r\n{\"A\":1,\"B\":\"str\",\"foobarId\":1}", Result= + "foobar\r\n{\"A\":1,\"B\":\"str\",\"foobarId\":1}\r\n{\"A\":1,\"B\":\"str\",\"foobarId\":2}")] + public string Inserts(string initialContents) + { + TextFile.Spit(_file, initialContents); + var subject = new JsonDB(_file); + subject.Insert("foobar", _foobar); + return TextFile.Slurp(_file).TrimEnd(); + } + + [TestCase("foobar\r\n{\"A\":1,\"B\":\"str\"}", "([A,1])", Result="")] + [TestCase("foobar\r\n{\"A\":1,\"B\":\"str\"}\r\n{\"A\":2,\"B\":\"QQQ\"}", "([A,1])", + Result = "foobar\r\n{\"A\":2,\"B\":\"QQQ\"}")] + public string Deletes(string initialContents, string keys) + { + TextFile.Spit(_file, initialContents); + var subject = new JsonDB(_file); + + var keyDict = Read.Dict(keys).SelectVals(TypeConversion.Infervert); + + subject.Delete("foobar", keyDict); + return TextFile.Slurp(_file).TrimEnd(); + } + + [TestCase("", "([B,QQQ])", "([A,1])", Result = "")] + [TestCase("foobar\r\n{\"A\":1,\"B\":\"str\"}", "([B,QQQ])", "([A,1])", + Result = "foobar\r\n{\"A\":1,\"B\":\"QQQ\"}")] + [TestCase("foobar\r\n{\"A\":1,\"B\":\"str\"}", "([B,QQQ])", "([A,2])", + Result = "foobar\r\n{\"A\":1,\"B\":\"str\"}")] + [TestCase("foobar\r\n{\"A\":1,\"B\":\"str\"}\r\n{\"A\":2,\"B\":\"str\"}", "([B,QQQ])", "([A,1])", + Result = "foobar\r\n{\"A\":2,\"B\":\"str\"}\r\n{\"A\":1,\"B\":\"QQQ\"}")] + [TestCase("foobar\r\n{\"A\":1,\"B\":\"str\"}\r\n{\"A\":2,\"B\":\"str\"}", "([B,QQQ])", "([B,str])", + Result = "foobar\r\n{\"A\":1,\"B\":\"QQQ\"}\r\n{\"A\":2,\"B\":\"QQQ\"}")] + public string Updates(string initialContents, string data, string keys) + { + TextFile.Spit(_file, initialContents); + var subject = new JsonDB(_file); + + var keyDict = Read.Dict(keys).SelectVals(TypeConversion.Infervert); + var dataDict = Read.Dict(data).SelectVals(TypeConversion.Infervert); + + subject.Update("foobar", dataDict, keyDict); + return TextFile.Slurp(_file).TrimEnd(); + } + + [TestCase("", "()", "", Result="()")] + [TestCase("foobar\r\n{\"A\":1,\"B\":\"str\"}", "([A,1])", "", Result="(([A,1],[B,str]))")] + [TestCase("foobar\r\n{\"A\":1,\"B\":\"str\"}", "([A,2])", "", Result = "()")] + [TestCase("foobar\r\n{\"A\":1,\"B\":\"str\"}", "([A,1])", "A", Result = "(([A,1]))")] + [TestCase("foobar\r\n{\"A\":1,\"B\":\"str\"}\r\n{\"A\":2,\"B\":\"QQQ\"}", "([A,1])", "", Result = "(([A,1],[B,str]))")] + public string Reads(string initialContents, string keys, string fieldsToUse) + { + TextFile.Spit(_file, initialContents); + var subject = new JsonDB(_file); + + var keyDict = Read.Dict(keys).SelectVals(TypeConversion.Infervert); + var fields = Read.List(fieldsToUse); + + var res = subject.Read("foobar", keyDict, fields); + return res.Print(); + } + + [TestCase("table1\r\n{\"A\":1}\r\n{\"A\":2}\r\ntable2\r\n{\"B\":42}", "notTable", Result = "()")] + [TestCase("table1\r\n{\"A\":1}\r\n{\"A\":2}\r\ntable2\r\n{\"B\":42}", "table1", Result="(([A,1]),([A,2]))")] + [TestCase("table1\r\n{\"A\":1}\r\n{\"A\":2}\r\ntable2\r\n{\"B\":42}", "table2", Result = "(([B,42]))")] + public string Reads_MultipleTables(string initialContents, string table) + { + TextFile.Spit(_file, initialContents); + var subject = new JsonDB(_file); + + var res = subject.ReadAll(table); + return res.Print(); + } + + [TestCase("table1\r\n\r\n{\"A\":1}\r\n\r\n{\"A\":2}\r\ntable2\r\n\r\n{\"B\":42}", "notTable", Result = "()")] + [TestCase("table1\r\n\r\n{\"A\":1}\r\n\r\n{\"A\":2}\r\ntable2\r\n\r\n{\"B\":42}", "table1", Result = "(([A,1]),([A,2]))")] + [TestCase("table1\r\n\r\n{\"A\":1}\r\n\r\n{\"A\":2}\r\ntable2\r\n\r\n{\"B\":42}", "table2", Result = "(([B,42]))")] + public string Reads_WithWhitespace(string initialContents, string table) + { + TextFile.Spit(_file, initialContents); + var subject = new JsonDB(_file); + + var res = subject.ReadAll(table); + return res.Print(); + } + + [TestCase("table1\r\n\r\n{\"A\":1,\"B\":3}\r\n\r\n{\"A\":2,\"B\":3}", "([X,2])", "([A,1])", + Result = "(([A,2],[B,3]),([A,1],[B,3],[X,2]))")] + public string EnsureOnlyUpdatesOneRow(string initialContents, string data, string keys) + { + TextFile.Spit(_file, initialContents); + var subject = new JsonDB(_file); + + var ddata = Read.Dict(data); + var dkeys = Read.Dict(keys); + + subject.Ensure("table1", ddata, dkeys); + return subject.ReadAll("table1").Print(); + } + } +} diff --git a/test/Lasy.Tests.csproj b/test/Lasy.Tests/Lasy.Tests.csproj similarity index 51% rename from test/Lasy.Tests.csproj rename to test/Lasy.Tests/Lasy.Tests.csproj index 6b8e833..39fded8 100644 --- a/test/Lasy.Tests.csproj +++ b/test/Lasy.Tests/Lasy.Tests.csproj @@ -1,5 +1,5 @@  - + Debug AnyCPU @@ -10,8 +10,11 @@ Properties LasyTests LasyTests - v4.0 + v4.5.1 512 + ..\..\Lasy\ + true + true @@ -21,6 +24,7 @@ DEBUG;TRACE prompt 4 + false pdbonly @@ -29,14 +33,32 @@ TRACE prompt 4 + false - - False - ..\lib\nunit.framework.dll + + ..\..\packages\NUnit.2.6.3\lib\nunit.framework.dll + + + ..\..\packages\Nvelope.1.1.0.2\lib\net451\Nvelope.dll + + False + ..\..\packages\Rx-Core.2.2.2\lib\net45\System.Reactive.Core.dll + + + False + ..\..\packages\Rx-Interfaces.2.2.2\lib\net45\System.Reactive.Interfaces.dll + + + False + ..\..\packages\Rx-Linq.2.2.2\lib\net45\System.Reactive.Linq.dll + + + ..\..\packages\Rx-PlatformServices.2.2.3\lib\net45\System.Reactive.PlatformServices.dll + @@ -44,22 +66,34 @@ + + + + + + + + + + + + + + + + - + - - {AC4A93B6-DDB6-4FE1-B528-665DE101052B} - Nvelope - - + {99A415F9-8D5A-4977-AC8B-86EA82C891D3} Lasy @@ -78,7 +112,11 @@ + + + +