diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..24ba7f359 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,169 @@ +# editorconfig.org + +# top-most EditorConfig file +root = true + +# Default settings: +# A newline ending every file +# Use 4 spaces as indentation +[*] +insert_final_newline = true +indent_style = space +indent_size = 4 +end_of_line = lf + +[project.json] +indent_size = 2 + +# C# files +[*.cs] +end_of_line = lf +indent_style = tab +tab_width = 4 + +# New line preferences +csharp_new_line_before_open_brace = types:methods +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_within_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left + +# avoid this. unless absolutely necessary +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# only use var when it's obvious what the variable type is +csharp_style_var_for_built_in_types = false:none +csharp_style_var_when_type_is_apparent = false:none +csharp_style_var_elsewhere = false:suggestion + +# use language keywords instead of BCL types +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# name all constant fields using PascalCase +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style + +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const + +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# static fields should have s_ prefix +dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion +dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields +dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style + +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static + +dotnet_naming_style.static_prefix_style.required_prefix = s_ +dotnet_naming_style.static_prefix_style.capitalization = camel_case + +# internal and private fields should be _camelCase +dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion +dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields +dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style + +dotnet_naming_symbols.private_internal_fields.applicable_kinds = field +dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal + +dotnet_naming_style.camel_case_underscore_style.required_prefix = _ +dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case + +# Code style defaults +dotnet_sort_system_directives_first = true +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = false + +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion + +# Expression-bodied members +csharp_style_expression_bodied_methods = false:none +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_operators = false:none +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none + +# Pattern matching +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion + +# Null checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = do_not_ignore +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = true +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = true +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# C++ Files +[*.{cpp,h,in}] +curly_bracket_next_line = true +indent_brace_style = Allman + +# Xml project files +[*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] +indent_size = 2 +end_of_line = crlf + +# Xml build files +[*.builds] +indent_size = 2 + +# Xml files +[*.{xml,stylecop,resx,ruleset}] +indent_size = 2 + +# Xml config files +[*.{props,targets,config,nuspec}] +indent_size = 2 + +# Shell scripts +[*.sh] +end_of_line = lf +[*.{cmd, bat}] +end_of_line = crlf + +[Makefile] +indent_style = tab diff --git a/.gitignore b/.gitignore index 406546f6e..ddf273be9 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ bld/ .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ +dist/ # MSTest test Results [Tt]est[Rr]esult*/ @@ -46,6 +47,7 @@ dlldata.c # DNX project.lock.json +project.fragment.lock.json artifacts/ *_i.c @@ -84,6 +86,8 @@ ipch/ *.opensdf *.sdf *.cachefile +*.VC.db +*.VC.VC.opendb # Visual Studio profiler *.psess @@ -142,11 +146,16 @@ publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings +# TODO: Comment the next line if you want to checkin your web deploy settings # but database connection strings (with potential passwords) will be unencrypted -*.pubxml +#*.pubxml *.publishproj +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore @@ -167,12 +176,11 @@ csx/ ecf/ rcf/ -# Microsoft Azure ApplicationInsights config file -ApplicationInsights.config - -# Windows Store app package directory +# Windows Store app package directories and files AppPackages/ BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt # Visual Studio cache files # files ending in .cache can be ignored @@ -186,11 +194,16 @@ ClientBin/ *~ *.dbmdl *.dbproj.schemaview +*.jfm *.pfx *.publishsettings node_modules/ orleans.codegen.cs +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) + + # RIA/Silverlight projects Generated_Code/ @@ -236,6 +249,15 @@ _Pvt_Extensions # Paket dependency manager .paket/paket.exe +paket-files/ # FAKE - F# Make .fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + diff --git a/Logo-low.png b/Logo-low.png new file mode 100644 index 000000000..f3ae12480 Binary files /dev/null and b/Logo-low.png differ diff --git a/Makefile b/Makefile index 08ac68739..53ad35395 100644 --- a/Makefile +++ b/Makefile @@ -1,21 +1,35 @@ - +SRC=src/SQLite.cs src/SQLiteAsync.cs all: test nuget -test: tests/bin/Debug/SQLite.Tests.dll +test: tests/bin/Debug/SQLite.Tests.dll tests/ApiDiff/bin/Debug/ApiDiff.exe nunit-console tests/bin/Debug/SQLite.Tests.dll + mono tests/ApiDiff/bin/Debug/ApiDiff.exe -tests/bin/Debug/SQLite.Tests.dll: tests/SQLite.Tests.csproj src/SQLite.cs src/SQLiteAsync.cs - xbuild tests/SQLite.Tests.csproj +tests/bin/Debug/SQLite.Tests.dll: tests/SQLite.Tests.csproj $(SRC) + msbuild tests/SQLite.Tests.csproj -nuget: srcnuget pclnuget +tests/ApiDiff/bin/Debug/ApiDiff.exe: tests/ApiDiff/ApiDiff.csproj $(SRC) + msbuild tests/ApiDiff/ApiDiff.csproj -srcnuget: src/SQLite.cs src/SQLiteAsync.cs sqlite-net.nuspec - nuget pack sqlite-net.nuspec +nuget: srcnuget pclnuget basenuget sqlciphernuget -pclnuget: src/SQLite.cs src/SQLiteAsync.cs +packages: nuget/SQLite-net/packages.config nuget restore SQLite.sln - '/Applications/Xamarin Studio.app/Contents/MacOS/mdtool' build '-c:Release|iPhone' SQLite.sln -p:SQLite-net + +srcnuget: sqlite-net.nuspec $(SRC) + nuget pack sqlite-net.nuspec + +pclnuget: sqlite-net-pcl.nuspec packages $(SRC) + msbuild "/p:Configuration=Release" nuget/SQLite-net/SQLite-net.csproj + msbuild "/p:Configuration=Release" nuget/SQLite-net-std/SQLite-net-std.csproj nuget pack sqlite-net-pcl.nuspec -o .\ +basenuget: sqlite-net-pcl.nuspec packages $(SRC) + msbuild "/p:Configuration=Release" nuget/SQLite-net-base/SQLite-net-base.csproj + nuget pack sqlite-net-base.nuspec -o .\ + +sqlciphernuget: sqlite-net-sqlcipher.nuspec packages $(SRC) + msbuild "/p:Configuration=Release" nuget/SQLite-net-sqlcipher/SQLite-net-sqlcipher.csproj + nuget pack sqlite-net-sqlcipher.nuspec -o .\ diff --git a/README.md b/README.md index 5c2e769f8..500bf1973 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,6 @@ public class Stock { [PrimaryKey, AutoIncrement] public int Id { get; set; } - [MaxLength(8)] public string Symbol { get; set; } } @@ -69,7 +68,10 @@ Both APIs are explained in the two sections below. Once you have defined your entity, you can automatically generate tables in your database by calling `CreateTable`: ```csharp -var db = new SQLiteConnection("foofoo"); +// Get an absolute path to the database file +var databasePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "MyData.db"); + +var db = new SQLiteConnection(databasePath); db.CreateTable(); db.CreateTable(); ``` @@ -90,7 +92,7 @@ Similar methods exist for `Update` and `Delete`. The most straightforward way to query for data is using the `Table` method. This can take predicates for constraining via WHERE clauses and/or adding ORDER BY clauses: ```csharp -var conn = new SQLiteConnection("foofoo"); +var conn = new SQLiteConnection(databasePath); var query = conn.Table().Where(v => v.Symbol.StartsWith("A")); foreach (var stock in query) @@ -100,8 +102,7 @@ foreach (var stock in query) You can also query the database at a low-level using the `Query` method: ```csharp -public static IEnumerable QueryValuations (SQLiteConnection db, Stock stock) -{ +public static IEnumerable QueryValuations (SQLiteConnection db, Stock stock) { return db.Query ("select * from Valuation where StockId = ?", stock.Id); } ``` @@ -109,13 +110,14 @@ public static IEnumerable QueryValuations (SQLiteConnection db, Stock The generic parameter to the `Query` method specifies the type of object to create for each row. It can be one of your table classes, or any other class whose public properties match the column returned by the query. For instance, we could rewrite the above query as: ```csharp -public class Val { +public class Val +{ public decimal Money { get; set; } public DateTime Date { get; set; } } -public static IEnumerable QueryVals (SQLiteConnection db, Stock stock) -{ - return db.Query ("select 'Price' as 'Money', 'Time' as 'Date' from Valuation where StockId = ?", stock.Id); + +public static IEnumerable QueryVals (SQLiteConnection db, Stock stock) { + return db.Query ("select \"Price\" as \"Money\", \"Time\" as \"Date\" from Valuation where StockId = ?", stock.Id); } ``` @@ -129,7 +131,10 @@ will work for you. Once you have defined your entity, you can automatically generate tables by calling `CreateTableAsync`: ```csharp -var conn = new SQLiteAsyncConnection("foofoo"); +// Get an absolute path to the database file +var databasePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "MyData.db"); + +var conn = new SQLiteAsyncConnection(databasePath); await conn.CreateTableAsync(); @@ -144,7 +149,7 @@ Stock stock = new Stock() Symbol = "AAPL" }; -var conn = new SQLiteAsyncConnection("foofoo"); +var conn = new SQLiteAsyncConnection(databasePath); await conn.InsertAsync(stock); @@ -158,7 +163,7 @@ you can add predictates for constraining via WHERE clauses and/or adding ORDER B retrieval methods - `ToListAsync`, `FirstAsync`, or `FirstOrDefaultAsync` - is called. ```csharp -var conn = new SQLiteAsyncConnection("foofoo"); +var conn = new SQLiteAsyncConnection(databasePath); var query = await conn.Table().Where(v => v.Symbol.StartsWith("A")); var result = await query.ToListAsync(); @@ -173,7 +178,7 @@ operations provided by `InsertAsync` etc you can issue `ExecuteAsync` methods to Another helpful method is `ExecuteScalarAsync`. This allows you to return a scalar value from the database easily: ```csharp -var conn = new SQLiteAsyncConnection("foofoo"); +var conn = new SQLiteAsyncConnection(databasePath); var result = await conn.ExecuteScalarAsync("select count(*) from Stock"); @@ -184,8 +189,9 @@ Debug.WriteLine(string.Format("Found '{0}' stock items.", result)); You can add support for encrypted databases using SQLCipher by including an additional package [SQLitePCLRaw.bundle_sqlcipher](https://www.nuget.org/packages/SQLitePCLRaw.bundle_sqlcipher/). -I'll let [Eric Sink explain](https://github.com/ericsink/SQLitePCL.raw/wiki/How-to-use-SQLCipher-with-SQLite-net): +I'll let [Eric Sink explain how to use SQLCipher with SQLite-net](https://github.com/ericsink/SQLitePCL.raw/wiki/How-to-use-SQLCipher-with-SQLite-net): +> The reference to bundle_sqlcipher must be placed in your app project. > What happens here is that SQLite-net references bundle_green, but at build time, bundle_sqlcipher gets substituted in its place. ## Thank you! diff --git a/SQLite.sln b/SQLite.sln index 7cc6f835a..efe1f8f7e 100644 --- a/SQLite.sln +++ b/SQLite.sln @@ -7,13 +7,23 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject sqlite-net-pcl.nuspec = sqlite-net-pcl.nuspec sqlite-net.nuspec = sqlite-net.nuspec + Makefile = Makefile + sqlite-net-base.nuspec = sqlite-net-base.nuspec EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SQLite-net", "nuget\SQLite-net\SQLite-net.csproj", "{7F946494-8EE0-432B-8A87-98961143D5C1}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{FECC0E44-E626-49CB-BD8B-0CFBD93FBEFF}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SQLite.Tests.iOS", "tests\SQLite.Tests.iOS\SQLite.Tests.iOS.csproj", "{81850129-71C3-40C7-A48B-AA5D2C2E365E}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SQLiteTestsiOS", "tests\SQLite.Tests.iOS\SQLiteTestsiOS.csproj", "{81850129-71C3-40C7-A48B-AA5D2C2E365E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SQLite-net-std", "nuget\SQLite-net-std\SQLite-net-std.csproj", "{081D08D6-10F1-431B-88FE-469FD9FE898C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiDiff", "tests\ApiDiff\ApiDiff.csproj", "{1DEF735C-B973-4ED9-8446-7FFA6D0B410B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SQLite-net-base", "nuget\SQLite-net-base\SQLite-net-base.csproj", "{53D1953C-3641-47D0-BE08-14DB853CC576}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SQLite-net-sqlcipher", "nuget\SQLite-net-sqlcipher\SQLite-net-sqlcipher.csproj", "{59DB03EF-E28D-431E-9058-74AF316800EE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -61,12 +71,78 @@ Global {81850129-71C3-40C7-A48B-AA5D2C2E365E}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator {81850129-71C3-40C7-A48B-AA5D2C2E365E}.Debug|iPhone.ActiveCfg = Debug|iPhone {81850129-71C3-40C7-A48B-AA5D2C2E365E}.Debug|iPhone.Build.0 = Debug|iPhone + {081D08D6-10F1-431B-88FE-469FD9FE898C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {081D08D6-10F1-431B-88FE-469FD9FE898C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {081D08D6-10F1-431B-88FE-469FD9FE898C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {081D08D6-10F1-431B-88FE-469FD9FE898C}.Release|Any CPU.Build.0 = Release|Any CPU + {081D08D6-10F1-431B-88FE-469FD9FE898C}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {081D08D6-10F1-431B-88FE-469FD9FE898C}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {081D08D6-10F1-431B-88FE-469FD9FE898C}.Release|iPhone.ActiveCfg = Release|Any CPU + {081D08D6-10F1-431B-88FE-469FD9FE898C}.Release|iPhone.Build.0 = Release|Any CPU + {081D08D6-10F1-431B-88FE-469FD9FE898C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {081D08D6-10F1-431B-88FE-469FD9FE898C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {081D08D6-10F1-431B-88FE-469FD9FE898C}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {081D08D6-10F1-431B-88FE-469FD9FE898C}.Debug|iPhone.Build.0 = Debug|Any CPU + {1DEF735C-B973-4ED9-8446-7FFA6D0B410B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1DEF735C-B973-4ED9-8446-7FFA6D0B410B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1DEF735C-B973-4ED9-8446-7FFA6D0B410B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1DEF735C-B973-4ED9-8446-7FFA6D0B410B}.Release|Any CPU.Build.0 = Release|Any CPU + {1DEF735C-B973-4ED9-8446-7FFA6D0B410B}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {1DEF735C-B973-4ED9-8446-7FFA6D0B410B}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {1DEF735C-B973-4ED9-8446-7FFA6D0B410B}.Release|iPhone.ActiveCfg = Release|Any CPU + {1DEF735C-B973-4ED9-8446-7FFA6D0B410B}.Release|iPhone.Build.0 = Release|Any CPU + {1DEF735C-B973-4ED9-8446-7FFA6D0B410B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {1DEF735C-B973-4ED9-8446-7FFA6D0B410B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {1DEF735C-B973-4ED9-8446-7FFA6D0B410B}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {1DEF735C-B973-4ED9-8446-7FFA6D0B410B}.Debug|iPhone.Build.0 = Debug|Any CPU + {53D1953C-3641-47D0-BE08-14DB853CC576}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {53D1953C-3641-47D0-BE08-14DB853CC576}.Debug|Any CPU.Build.0 = Debug|Any CPU + {53D1953C-3641-47D0-BE08-14DB853CC576}.Release|Any CPU.ActiveCfg = Release|Any CPU + {53D1953C-3641-47D0-BE08-14DB853CC576}.Release|Any CPU.Build.0 = Release|Any CPU + {53D1953C-3641-47D0-BE08-14DB853CC576}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {53D1953C-3641-47D0-BE08-14DB853CC576}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {53D1953C-3641-47D0-BE08-14DB853CC576}.Release|iPhone.ActiveCfg = Release|Any CPU + {53D1953C-3641-47D0-BE08-14DB853CC576}.Release|iPhone.Build.0 = Release|Any CPU + {53D1953C-3641-47D0-BE08-14DB853CC576}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {53D1953C-3641-47D0-BE08-14DB853CC576}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {53D1953C-3641-47D0-BE08-14DB853CC576}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {53D1953C-3641-47D0-BE08-14DB853CC576}.Debug|iPhone.Build.0 = Debug|Any CPU + {59DB03EF-E28D-431E-9058-74AF316800EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {59DB03EF-E28D-431E-9058-74AF316800EE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {59DB03EF-E28D-431E-9058-74AF316800EE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {59DB03EF-E28D-431E-9058-74AF316800EE}.Release|Any CPU.Build.0 = Release|Any CPU + {59DB03EF-E28D-431E-9058-74AF316800EE}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {59DB03EF-E28D-431E-9058-74AF316800EE}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {59DB03EF-E28D-431E-9058-74AF316800EE}.Release|iPhone.ActiveCfg = Release|Any CPU + {59DB03EF-E28D-431E-9058-74AF316800EE}.Release|iPhone.Build.0 = Release|Any CPU + {59DB03EF-E28D-431E-9058-74AF316800EE}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {59DB03EF-E28D-431E-9058-74AF316800EE}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {59DB03EF-E28D-431E-9058-74AF316800EE}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {59DB03EF-E28D-431E-9058-74AF316800EE}.Debug|iPhone.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {6947A8F1-99BE-4DD1-AD4D-D89425CE67A2} = {FECC0E44-E626-49CB-BD8B-0CFBD93FBEFF} {81850129-71C3-40C7-A48B-AA5D2C2E365E} = {FECC0E44-E626-49CB-BD8B-0CFBD93FBEFF} + {1DEF735C-B973-4ED9-8446-7FFA6D0B410B} = {FECC0E44-E626-49CB-BD8B-0CFBD93FBEFF} EndGlobalSection GlobalSection(MonoDevelopProperties) = preSolution StartupItem = tests\SQLite.Tests.csproj + Policies = $0 + $0.TextStylePolicy = $1 + $1.FileWidth = 128 + $1.NoTabsAfterNonTabs = True + $1.EolMarker = Unix + $1.scope = text/x-csharp + $0.CSharpFormattingPolicy = $2 + $2.NewLinesForBracesInProperties = False + $2.NewLinesForBracesInAccessors = False + $2.NewLinesForBracesInAnonymousMethods = False + $2.NewLinesForBracesInControlBlocks = False + $2.NewLinesForBracesInAnonymousTypes = False + $2.NewLinesForBracesInObjectCollectionArrayInitializers = False + $2.NewLinesForBracesInLambdaExpressionBody = False + $2.scope = text/x-csharp + $2.SpacingAfterMethodDeclarationName = True + $2.SpaceAfterMethodCallName = True EndGlobalSection EndGlobal diff --git a/nuget/SQLite-net-base/SQLite-net-base.csproj b/nuget/SQLite-net-base/SQLite-net-base.csproj new file mode 100644 index 000000000..26074ed9f --- /dev/null +++ b/nuget/SQLite-net-base/SQLite-net-base.csproj @@ -0,0 +1,35 @@ + + + + netstandard1.1 + SQLite-net + 1.0.0 + SQLite-net Official .NET Standard Base Library + Light weight library providing easy SQLite database storage + Krueger Systems, Inc. + + + + USE_SQLITEPCL_RAW;NO_SQLITEPCL_RAW_BATTERIES;DEBUG;NETSTANDARD1_1 + true + bin\Debug\netstandard1.1\SQLite-net.xml + + + false + + USE_SQLITEPCL_RAW;NO_SQLITEPCL_RAW_BATTERIES;RELEASE;NETSTANDARD1_1 + true + bin\Release\netstandard1.1\SQLite-net.xml + + + + SQLite.cs + + + SQLiteAsync.cs + + + + + + diff --git a/nuget/SQLite-net-sqlcipher/SQLite-net-sqlcipher.csproj b/nuget/SQLite-net-sqlcipher/SQLite-net-sqlcipher.csproj new file mode 100644 index 000000000..c49a05589 --- /dev/null +++ b/nuget/SQLite-net-sqlcipher/SQLite-net-sqlcipher.csproj @@ -0,0 +1,35 @@ + + + + netstandard1.1 + SQLite-net + 1.0.0 + SQLite-net Official SQLCipher .NET Standard Library + Light weight library providing easy SQLite database storage + Krueger Systems, Inc. + + + + false + + USE_SQLITEPCL_RAW;RELEASE;NETSTANDARD1_1 + true + bin\Release\netstandard1.1\SQLite-net.xml + + + USE_SQLITEPCL_RAW;DEBUG;NETSTANDARD1_1 + true + bin\Debug\netstandard1.1\SQLite-net.xml + + + + + + + SQLite.cs + + + SQLiteAsync.cs + + + diff --git a/nuget/SQLite-net-std/SQLite-net-std.csproj b/nuget/SQLite-net-std/SQLite-net-std.csproj new file mode 100644 index 000000000..1e2f21a62 --- /dev/null +++ b/nuget/SQLite-net-std/SQLite-net-std.csproj @@ -0,0 +1,35 @@ + + + + netstandard1.1 + SQLite-net + 1.0.0 + SQLite-net Official .NET Standard Library + Light weight library providing easy SQLite database storage + Krueger Systems, Inc. + + + + false + + USE_SQLITEPCL_RAW;RELEASE;NETSTANDARD1_1 + true + bin\Release\netstandard1.1\SQLite-net.xml + + + USE_SQLITEPCL_RAW;DEBUG;NETSTANDARD1_1 + true + bin\Debug\netstandard1.1\SQLite-net.xml + + + + + + + SQLite.cs + + + SQLiteAsync.cs + + + diff --git a/nuget/SQLite-net/SQLite-net.csproj b/nuget/SQLite-net/SQLite-net.csproj index a26f07de0..3575ebfd7 100644 --- a/nuget/SQLite-net/SQLite-net.csproj +++ b/nuget/SQLite-net/SQLite-net.csproj @@ -23,19 +23,21 @@ full false bin\Debug\ - TRACE;DEBUG;USE_SQLITEPCL_RAW USE_NEW_REFLECTION_API NO_CONCURRENT + DEBUG;USE_SQLITEPCL_RAW prompt 4 + true bin\Debug\SQLite-net.xml - true + false true bin\Release\ - TRACE;USE_SQLITEPCL_RAW USE_NEW_REFLECTION_API NO_CONCURRENT + RELEASE;USE_SQLITEPCL_RAW prompt 4 + true bin\Release\SQLite-net.xml @@ -54,13 +56,13 @@ - ..\..\packages\SQLitePCLRaw.core.1.1.2\lib\portable-net45+netcore45+wpa81+wp8+MonoAndroid10+MonoTouch10+Xamarin.iOS10\SQLitePCLRaw.core.dll + ..\..\packages\SQLitePCLRaw.core.1.1.8\lib\portable-net45+netcore45+wpa81+wp8+MonoAndroid10+MonoTouch10+Xamarin.iOS10\SQLitePCLRaw.core.dll - ..\..\packages\SQLitePCLRaw.bundle_green.1.1.2\lib\portable-net45+netcore45+wpa81+wp8+MonoAndroid10+MonoTouch10+Xamarin.iOS10\SQLitePCLRaw.batteries_green.dll + ..\..\packages\SQLitePCLRaw.bundle_green.1.1.8\lib\portable-net45+netcore45+wpa81+wp8+MonoAndroid10+MonoTouch10+Xamarin.iOS10\SQLitePCLRaw.batteries_green.dll - ..\..\packages\SQLitePCLRaw.bundle_green.1.1.2\lib\portable-net45+netcore45+wpa81+wp8+MonoAndroid10+MonoTouch10+Xamarin.iOS10\SQLitePCLRaw.batteries_v2.dll + ..\..\packages\SQLitePCLRaw.bundle_green.1.1.8\lib\portable-net45+netcore45+wpa81+wp8+MonoAndroid10+MonoTouch10+Xamarin.iOS10\SQLitePCLRaw.batteries_v2.dll diff --git a/nuget/SQLite-net/packages.config b/nuget/SQLite-net/packages.config index 5165d0036..10c88ccf8 100644 --- a/nuget/SQLite-net/packages.config +++ b/nuget/SQLite-net/packages.config @@ -1,5 +1,5 @@  - - + + \ No newline at end of file diff --git a/sqlite-net-base.nuspec b/sqlite-net-base.nuspec new file mode 100644 index 000000000..74c0295c1 --- /dev/null +++ b/sqlite-net-base.nuspec @@ -0,0 +1,67 @@ + + + + 1.0.0 + Frank A. Krueger + Frank A. Krueger + https://raw.githubusercontent.com/praeclarum/sqlite-net/master/LICENSE.md + https://github.com/praeclarum/sqlite-net + https://raw.githubusercontent.com/praeclarum/sqlite-net/master/nuget/Logo-low.png + sqlite-net-base + SQLite-net Base Library + SQLite-net Official Portable Library (Base) is the easy and configurable way to access sqlite from .NET apps. + false + + This is a special version of SQLite-net-pcl that does not include a SQLitePCLRaw bundle. + It is meant to give you all the power of SQLite-net but with the freedom to choose your own provider. + Please use the package sqlite-net-pcl if you have no idea what any of this means. + + sqlite-net sqlite pcl database orm mobile raw + See project page + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sqlite-net-pcl.nuspec b/sqlite-net-pcl.nuspec index 6f1be12e2..236ac6ec4 100644 --- a/sqlite-net-pcl.nuspec +++ b/sqlite-net-pcl.nuspec @@ -1,39 +1,64 @@ - 1.3.1 + 1.0.0 Frank A. Krueger Frank A. Krueger https://raw.githubusercontent.com/praeclarum/sqlite-net/master/LICENSE.md https://github.com/praeclarum/sqlite-net https://raw.githubusercontent.com/praeclarum/sqlite-net/master/nuget/Logo-low.png sqlite-net-pcl - SQLite-net PCL - The official portable version of sqlite-net: the easy way to access sqlite from .NET apps. + SQLite-net Official Portable Library + SQLite-net Official Portable Library is the easy way to access sqlite from .NET apps. false SQLite-net is an open source and light weight library providing easy SQLite database storage for .NET, Mono, and Xamarin applications. This version uses SQLitePCLRaw to provide platform independent versions of SQLite. - sqlite pcl database orm mobile - - - + sqlite-net sqlite pcl database orm mobile raw + See project page - - + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + diff --git a/sqlite-net-sqlcipher.nuspec b/sqlite-net-sqlcipher.nuspec new file mode 100644 index 000000000..93a191580 --- /dev/null +++ b/sqlite-net-sqlcipher.nuspec @@ -0,0 +1,63 @@ + + + + 1.0.0 + Frank A. Krueger + Frank A. Krueger + https://raw.githubusercontent.com/praeclarum/sqlite-net/master/LICENSE.md + https://github.com/praeclarum/sqlite-net + https://raw.githubusercontent.com/praeclarum/sqlite-net/master/nuget/Logo-low.png + sqlite-net-sqlcipher + SQLite-net Official SQLCipher .NET Standard Library + SQLite-net Official Portable Library is the easy way to access sqlite from .NET apps. + false + SQLite-net is an open source and light weight library providing easy SQLite database storage for .NET, Mono, and Xamarin applications. This version uses SQLitePCLRaw to provide platform independent versions of SQLite with the SQLCipher extension. This enables secure access to the datbase with password (key) access. + sqlite-net sqlite pcl database orm mobile encryption sqlcipher + See project page + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sqlite-net.nuspec b/sqlite-net.nuspec index e798f4835..44352f2a3 100644 --- a/sqlite-net.nuspec +++ b/sqlite-net.nuspec @@ -1,7 +1,7 @@ - 1.2.1 + 1.0.0 Frank Krueger Frank Krueger,Tim Heuer https://raw.githubusercontent.com/praeclarum/sqlite-net/master/LICENSE.md @@ -11,13 +11,9 @@ sqlite-net false sqlite-net is an open source, minimal library to allow .NET and Mono applications to store data in SQLite databases. It is written in C# 3.0 and is meant to be simply compiled in with your projects. It was first designed to work with MonoTouch on the iPhone, but should work in any other CLI environment. - sqlite sql monotouch database metro winrt - A .NET client library to access SQLite embedded database files in a LINQ manner. - - - + sqlite-net sqlite sql monotouch database metro winrt + A .NET client library to access SQLite embedded database files in a LINQ manner. + See project page diff --git a/src/AssemblyInfo.cs b/src/AssemblyInfo.cs index dcc0d06aa..9daa3e6a1 100644 --- a/src/AssemblyInfo.cs +++ b/src/AssemblyInfo.cs @@ -6,12 +6,11 @@ // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("SQLite-net PCL")] -[assembly: AssemblyDescription("")] +[assembly: AssemblyTitle("SQLite-net Official Portable Library")] +[assembly: AssemblyDescription("Light weight library providing easy SQLite database storage")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Krueger Systems, Inc.")] [assembly: AssemblyProduct("SQLite-net")] -[assembly: AssemblyCopyright("Copyright © 2009-2016 Krueger Systems, Inc.")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] @@ -26,5 +25,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.1.0.0")] -[assembly: AssemblyFileVersion("1.1.0.0")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/SQLite.cs b/src/SQLite.cs index 7ef11fb6f..c276066fe 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -1,16 +1,16 @@ // -// Copyright (c) 2009-2016 Krueger Systems, Inc. -// +// Copyright (c) 2009-2017 Krueger Systems, Inc. +// // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -23,25 +23,17 @@ #define USE_CSHARP_SQLITE #endif -#if NETFX_CORE || NETCORE -#define USE_NEW_REFLECTION_API -#endif - using System; +using System.Collections; using System.Diagnostics; #if !USE_SQLITEPCL_RAW using System.Runtime.InteropServices; #endif using System.Collections.Generic; -#if NO_CONCURRENT -using ConcurrentStringDictionary = System.Collections.Generic.Dictionary; -using SQLite.Extensions; -#else -using ConcurrentStringDictionary = System.Collections.Concurrent.ConcurrentDictionary; -#endif using System.Reflection; using System.Linq; using System.Linq.Expressions; +using System.Text; using System.Threading; #if USE_CSHARP_SQLITE @@ -69,7 +61,7 @@ public class SQLiteException : Exception { public SQLite3.Result Result { get; private set; } - protected SQLiteException (SQLite3.Result r,string message) : base(message) + protected SQLiteException (SQLite3.Result r, string message) : base (message) { Result = r; } @@ -117,7 +109,8 @@ public static NotNullConstraintViolationException New (SQLiteException exception } [Flags] - public enum SQLiteOpenFlags { + public enum SQLiteOpenFlags + { ReadOnly = 1, ReadWrite = 2, Create = 4, NoMutex = 0x8000, FullMutex = 0x10000, SharedCache = 0x20000, PrivateCache = 0x40000, @@ -127,27 +120,52 @@ public enum SQLiteOpenFlags { ProtectionNone = 0x00400000 } - [Flags] - public enum CreateFlags - { - None = 0x000, - ImplicitPK = 0x001, // create a primary key for field called 'Id' (Orm.ImplicitPkName) - ImplicitIndex = 0x002, // create an index for fields ending in 'Id' (Orm.ImplicitIndexSuffix) - AllImplicit = 0x003, // do both above - AutoIncPK = 0x004, // force PK field to be auto inc - FullTextSearch3 = 0x100, // create virtual table using FTS3 - FullTextSearch4 = 0x200 // create virtual table using FTS4 - } + [Flags] + public enum CreateFlags + { + /// + /// Use the default creation options + /// + None = 0x000, + /// + /// Create a primary key index for a property called 'Id' (case-insensitive). + /// This avoids the need for the [PrimaryKey] attribute. + /// + ImplicitPK = 0x001, + /// + /// Create indices for properties ending in 'Id' (case-insensitive). + /// + ImplicitIndex = 0x002, + /// + /// Create a primary key for a property called 'Id' and + /// create an indices for properties ending in 'Id' (case-insensitive). + /// + AllImplicit = 0x003, + /// + /// Force the primary key property to be auto incrementing. + /// This avoids the need for the [AutoIncrement] attribute. + /// The primary key property on the class should have type int or long. + /// + AutoIncPK = 0x004, + /// + /// Create virtual table using FTS3 + /// + FullTextSearch3 = 0x100, + /// + /// Create virtual table using FTS4 + /// + FullTextSearch4 = 0x200 + } /// - /// Represents an open connection to a SQLite database. + /// An open connection to a SQLite database. /// + [Preserve (AllMembers = true)] public partial class SQLiteConnection : IDisposable { private bool _open; private TimeSpan _busyTimeout; - private Dictionary _mappings = null; - private Dictionary _tables = null; + readonly static Dictionary _mappings = new Dictionary (); private System.Diagnostics.Stopwatch _sw; private long _elapsedMilliseconds = 0; @@ -155,20 +173,44 @@ public partial class SQLiteConnection : IDisposable private Random _rand = new Random (); public Sqlite3DatabaseHandle Handle { get; private set; } - internal static readonly Sqlite3DatabaseHandle NullHandle = default(Sqlite3DatabaseHandle); + static readonly Sqlite3DatabaseHandle NullHandle = default (Sqlite3DatabaseHandle); + /// + /// Gets the database path used by this connection. + /// public string DatabasePath { get; private set; } + /// + /// Gets the SQLite library version number. 3007014 would be v3.7.14 + /// + public int LibVersionNumber { get; private set; } + + /// + /// Whether Trace lines should be written that show the execution time of queries. + /// public bool TimeExecution { get; set; } + /// + /// Whether to writer queries to during execution. + /// + /// The tracer. public bool Trace { get; set; } + /// + /// The delegate responsible for writing trace lines. + /// + /// The tracer. + public Action Tracer { get; set; } + + /// + /// Whether to store DateTime properties as ticks (true) or strings (false). + /// public bool StoreDateTimeAsTicks { get; private set; } -#if USE_SQLITEPCL_RAW - static SQLiteConnection() +#if USE_SQLITEPCL_RAW && !NO_SQLITEPCL_RAW_BATTERIES + static SQLiteConnection () { - SQLitePCL.Batteries_V2.Init(); + SQLitePCL.Batteries_V2.Init (); } #endif @@ -197,6 +239,9 @@ public SQLiteConnection (string databasePath, bool storeDateTimeAsTicks = true) /// /// Specifies the path to the database file. /// + /// + /// Flags controlling how the connection should be opened. + /// /// /// Specifies whether to store DateTime properties as ticks (true) or strings (false). You /// absolutely do want to store them as Ticks in all new projects. The value of false is @@ -207,11 +252,13 @@ public SQLiteConnection (string databasePath, bool storeDateTimeAsTicks = true) /// public SQLiteConnection (string databasePath, SQLiteOpenFlags openFlags, bool storeDateTimeAsTicks = true) { - if (string.IsNullOrEmpty (databasePath)) - throw new ArgumentException ("Must be specified", "databasePath"); + if (databasePath==null) + throw new ArgumentException ("Must be specified", nameof(databasePath)); DatabasePath = databasePath; + LibVersionNumber = SQLite3.LibVersionNumber (); + #if NETFX_CORE SQLite3.SetDirectory(/*temp directory type*/2, Windows.Storage.ApplicationData.Current.TemporaryFolder.Path); #endif @@ -219,7 +266,7 @@ public SQLiteConnection (string databasePath, SQLiteOpenFlags openFlags, bool st Sqlite3DatabaseHandle handle; #if SILVERLIGHT || USE_CSHARP_SQLITE || USE_SQLITEPCL_RAW - var r = SQLite3.Open (databasePath, out handle, (int)openFlags, IntPtr.Zero); + var r = SQLite3.Open (databasePath, out handle, (int)openFlags, IntPtr.Zero); #else // open using the byte[] // in the case where the path may include Unicode @@ -235,36 +282,67 @@ public SQLiteConnection (string databasePath, SQLiteOpenFlags openFlags, bool st _open = true; StoreDateTimeAsTicks = storeDateTimeAsTicks; - + BusyTimeout = TimeSpan.FromSeconds (0.1); + if (openFlags.HasFlag (SQLiteOpenFlags.ReadWrite)) { + ExecuteScalar ("PRAGMA journal_mode=WAL"); + } + Tracer = line => Debug.WriteLine (line); } - -#if __IOS__ - static SQLiteConnection () + + /// + /// Convert an input string to a quoted SQL string that can be safely used in queries. + /// + /// The quoted string. + /// The unsafe string to quote. + static string Quote (string unsafeString) { - if (_preserveDuringLinkMagic) { - var ti = new ColumnInfo (); - ti.Name = "magic"; - } + // TODO: Doesn't call sqlite3_mprintf("%Q", u) because we're waiting on https://github.com/ericsink/SQLitePCL.raw/issues/153 + if (unsafeString == null) return "NULL"; + var safe = unsafeString.Replace ("'", "''"); + return "'" + safe + "'"; } - /// - /// Used to list some code that we want the MonoTouch linker - /// to see, but that we never want to actually execute. + /// + /// Sets the key used to encrypt/decrypt the database. + /// This must be the first thing you call before doing anything else with this connection + /// if your database is encrypted. + /// This only has an effect if you are using the SQLCipher nuget package. /// - static bool _preserveDuringLinkMagic; -#endif + /// Ecryption key plain text that is converted to the real encryption key using PBKDF2 key derivation + public void SetKey (string key) + { + if (key == null) throw new ArgumentNullException (nameof (key)); + var q = Quote (key); + Execute ("pragma key = " + q); + } -#if !USE_SQLITEPCL_RAW - public void EnableLoadExtension(int onoff) - { - SQLite3.Result r = SQLite3.EnableLoadExtension(Handle, onoff); + /// + /// Sets the key used to encrypt/decrypt the database. + /// This must be the first thing you call before doing anything else with this connection + /// if your database is encrypted. + /// This only has an effect if you are using the SQLCipher nuget package. + /// + /// 256-bit (32 byte) ecryption key data + public void SetKey (byte[] key) + { + if (key == null) throw new ArgumentNullException (nameof (key)); + if (key.Length != 32) throw new ArgumentException ("Key must be 32 bytes (256-bit)", nameof(key)); + var s = String.Join ("", key.Select (x => x.ToString ("X2"))); + Execute ("pragma key = \"x'" + s + "'\""); + } + + /// + /// Enable or disable extension loading. + /// + public void EnableLoadExtension (bool enabled) + { + SQLite3.Result r = SQLite3.EnableLoadExtension (Handle, enabled ? 1 : 0); if (r != SQLite3.Result.OK) { string msg = SQLite3.GetErrmsg (Handle); throw SQLiteException.New (r, msg); } - } -#endif + } #if !USE_SQLITEPCL_RAW static byte[] GetNullTerminatedUtf8 (string s) @@ -276,7 +354,7 @@ static byte[] GetNullTerminatedUtf8 (string s) } #endif - /// + /// /// Sets a busy handler to sleep the specified amount of time when a table is locked. /// The handler will sleep multiple times until a total time of has accumulated. /// @@ -296,7 +374,9 @@ public TimeSpan BusyTimeout { /// public IEnumerable TableMappings { get { - return _tables != null ? _tables.Values : Enumerable.Empty (); + lock (_mappings) { + return new List (_mappings.Values); + } } } @@ -305,37 +385,46 @@ public IEnumerable TableMappings { /// /// /// The type whose mapping to the database is returned. - /// - /// + /// + /// /// Optional flags allowing implicit PK and indexes based on naming conventions - /// + /// /// - /// The mapping represents the schema of the columns of the database and contains + /// The mapping represents the schema of the columns of the database and contains /// methods to set and get properties of objects. /// - public TableMapping GetMapping(Type type, CreateFlags createFlags = CreateFlags.None) + public TableMapping GetMapping (Type type, CreateFlags createFlags = CreateFlags.None) { - if (_mappings == null) { - _mappings = new Dictionary (); - } TableMapping map; - if (!_mappings.TryGetValue (type.FullName, out map)) { - map = new TableMapping (type, createFlags); - _mappings [type.FullName] = map; + var key = type.FullName; + lock (_mappings) { + if (_mappings.TryGetValue (key, out map)) { + if (createFlags != CreateFlags.None && createFlags != map.CreateFlags) { + map = new TableMapping (type, createFlags); + _mappings[key] = map; + } + } + else { + map = new TableMapping (type, createFlags); + _mappings.Add (key, map); + } } return map; } - + /// /// Retrieves the mapping that is automatically generated for the given type. /// + /// + /// Optional flags allowing implicit PK and indexes based on naming conventions + /// /// - /// The mapping represents the schema of the columns of the database and contains + /// The mapping represents the schema of the columns of the database and contains /// methods to set and get properties of objects. /// - public TableMapping GetMapping () + public TableMapping GetMapping (CreateFlags createFlags = CreateFlags.None) { - return GetMapping (typeof (T)); + return GetMapping (typeof (T), createFlags); } private struct IndexedColumn @@ -355,15 +444,23 @@ private struct IndexInfo /// /// Executes a "drop table" on the database. This is non-recoverable. /// - public int DropTable() + public int DropTable () { - var map = GetMapping (typeof (T)); - - var query = string.Format("drop table if exists \"{0}\"", map.TableName); + return DropTable (GetMapping (typeof (T))); + } + /// + /// Executes a "drop table" on the database. This is non-recoverable. + /// + /// + /// The TableMapping used to identify the table. + /// + public int DropTable (TableMapping map) + { + var query = string.Format ("drop table if exists \"{0}\"", map.TableName); return Execute (query); } - + /// /// Executes a "create table if not exists" on the database. It also /// creates any specified indexes on the columns of the table. It uses @@ -371,11 +468,11 @@ public int DropTable() /// later access this schema by calling GetMapping. /// /// - /// The number of entries added to the database schema. + /// Whether the table was created or migrated. /// - public int CreateTable(CreateFlags createFlags = CreateFlags.None) + public CreateTableResult CreateTable (CreateFlags createFlags = CreateFlags.None) { - return CreateTable(typeof (T), createFlags); + return CreateTable (typeof (T), createFlags); } /// @@ -385,45 +482,48 @@ public int CreateTable(CreateFlags createFlags = CreateFlags.None) /// later access this schema by calling GetMapping. /// /// Type to reflect to a database table. - /// Optional flags allowing implicit PK and indexes based on naming conventions. + /// Optional flags allowing implicit PK and indexes based on naming conventions. /// - /// The number of entries added to the database schema. + /// Whether the table was created or migrated. /// - public int CreateTable(Type ty, CreateFlags createFlags = CreateFlags.None) + public CreateTableResult CreateTable (Type ty, CreateFlags createFlags = CreateFlags.None) { - if (_tables == null) { - _tables = new Dictionary (); - } - TableMapping map; - if (!_tables.TryGetValue (ty.FullName, out map)) { - map = GetMapping (ty, createFlags); - _tables.Add (ty.FullName, map); - } + var map = GetMapping (ty, createFlags); // Present a nice error if no columns specified if (map.Columns.Length == 0) { - throw new Exception (string.Format ("Cannot create a table with zero columns (does '{0}' have public properties?)", ty.FullName)); - } - - // Facilitate virtual tables a.k.a. full-text search. - bool fts3 = (createFlags & CreateFlags.FullTextSearch3) != 0; - bool fts4 = (createFlags & CreateFlags.FullTextSearch4) != 0; - bool fts = fts3 || fts4; - var @virtual = fts ? "virtual " : string.Empty; - var @using = fts3 ? "using fts3 " : fts4 ? "using fts4 " : string.Empty; - - // Build query. - var query = "create " + @virtual + "table if not exists \"" + map.TableName + "\" " + @using + "(\n"; - var decls = map.Columns.Select (p => Orm.SqlDecl (p, StoreDateTimeAsTicks)); - var decl = string.Join (",\n", decls.ToArray ()); - query += decl; - query += ")"; - - var count = Execute (query); - - if (count == 0) { //Possible bug: This always seems to return 0? - // Table already exists, migrate it - MigrateTable (map); + throw new Exception (string.Format ("Cannot create a table without columns (does '{0}' have public properties?)", ty.FullName)); + } + + // Check if the table exists + var result = CreateTableResult.Created; + var existingCols = GetTableInfo (map.TableName); + + // Create or migrate it + if (existingCols.Count == 0) { + + // Facilitate virtual tables a.k.a. full-text search. + bool fts3 = (createFlags & CreateFlags.FullTextSearch3) != 0; + bool fts4 = (createFlags & CreateFlags.FullTextSearch4) != 0; + bool fts = fts3 || fts4; + var @virtual = fts ? "virtual " : string.Empty; + var @using = fts3 ? "using fts3 " : fts4 ? "using fts4 " : string.Empty; + + // Build query. + var query = "create " + @virtual + "table if not exists \"" + map.TableName + "\" " + @using + "(\n"; + var decls = map.Columns.Select (p => Orm.SqlDecl (p, StoreDateTimeAsTicks)); + var decl = string.Join (",\n", decls.ToArray ()); + query += decl; + query += ")"; + if(map.WithoutRowId) { + query += " without rowid"; + } + + Execute (query); + } + else { + result = CreateTableResult.Migrated; + MigrateTable (map, existingCols); } var indexes = new Dictionary (); @@ -453,108 +553,195 @@ public int CreateTable(Type ty, CreateFlags createFlags = CreateFlags.None) foreach (var indexName in indexes.Keys) { var index = indexes[indexName]; - var columns = index.Columns.OrderBy(i => i.Order).Select(i => i.ColumnName).ToArray(); - count += CreateIndex(indexName, index.TableName, columns, index.Unique); + var columns = index.Columns.OrderBy (i => i.Order).Select (i => i.ColumnName).ToArray (); + CreateIndex (indexName, index.TableName, columns, index.Unique); } - - return count; + + return result; + } + + /// + /// Executes a "create table if not exists" on the database for each type. It also + /// creates any specified indexes on the columns of the table. It uses + /// a schema automatically generated from the specified type. You can + /// later access this schema by calling GetMapping. + /// + /// + /// Whether the table was created or migrated for each type. + /// + public CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) + where T : new() + where T2 : new() + { + return CreateTables (createFlags, typeof (T), typeof (T2)); + } + + /// + /// Executes a "create table if not exists" on the database for each type. It also + /// creates any specified indexes on the columns of the table. It uses + /// a schema automatically generated from the specified type. You can + /// later access this schema by calling GetMapping. + /// + /// + /// Whether the table was created or migrated for each type. + /// + public CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) + where T : new() + where T2 : new() + where T3 : new() + { + return CreateTables (createFlags, typeof (T), typeof (T2), typeof (T3)); + } + + /// + /// Executes a "create table if not exists" on the database for each type. It also + /// creates any specified indexes on the columns of the table. It uses + /// a schema automatically generated from the specified type. You can + /// later access this schema by calling GetMapping. + /// + /// + /// Whether the table was created or migrated for each type. + /// + public CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) + where T : new() + where T2 : new() + where T3 : new() + where T4 : new() + { + return CreateTables (createFlags, typeof (T), typeof (T2), typeof (T3), typeof (T4)); + } + + /// + /// Executes a "create table if not exists" on the database for each type. It also + /// creates any specified indexes on the columns of the table. It uses + /// a schema automatically generated from the specified type. You can + /// later access this schema by calling GetMapping. + /// + /// + /// Whether the table was created or migrated for each type. + /// + public CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) + where T : new() + where T2 : new() + where T3 : new() + where T4 : new() + where T5 : new() + { + return CreateTables (createFlags, typeof (T), typeof (T2), typeof (T3), typeof (T4), typeof (T5)); + } + + /// + /// Executes a "create table if not exists" on the database for each type. It also + /// creates any specified indexes on the columns of the table. It uses + /// a schema automatically generated from the specified type. You can + /// later access this schema by calling GetMapping. + /// + /// + /// Whether the table was created or migrated for each type. + /// + public CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None, params Type[] types) + { + var result = new CreateTablesResult (); + foreach (Type type in types) { + var aResult = CreateTable (type, createFlags); + result.Results[type] = aResult; + } + return result; + } + + /// + /// Creates an index for the specified table and columns. + /// + /// Name of the index to create + /// Name of the database table + /// An array of column names to index + /// Whether the index should be unique + public int CreateIndex (string indexName, string tableName, string[] columnNames, bool unique = false) + { + const string sqlFormat = "create {2} index if not exists \"{3}\" on \"{0}\"(\"{1}\")"; + var sql = String.Format (sqlFormat, tableName, string.Join ("\", \"", columnNames), unique ? "unique" : "", indexName); + return Execute (sql); + } + + /// + /// Creates an index for the specified table and column. + /// + /// Name of the index to create + /// Name of the database table + /// Name of the column to index + /// Whether the index should be unique + public int CreateIndex (string indexName, string tableName, string columnName, bool unique = false) + { + return CreateIndex (indexName, tableName, new string[] { columnName }, unique); + } + + /// + /// Creates an index for the specified table and column. + /// + /// Name of the database table + /// Name of the column to index + /// Whether the index should be unique + public int CreateIndex (string tableName, string columnName, bool unique = false) + { + return CreateIndex (tableName + "_" + columnName, tableName, columnName, unique); + } + + /// + /// Creates an index for the specified table and columns. + /// + /// Name of the database table + /// An array of column names to index + /// Whether the index should be unique + public int CreateIndex (string tableName, string[] columnNames, bool unique = false) + { + return CreateIndex (tableName + "_" + string.Join ("_", columnNames), tableName, columnNames, unique); } - /// - /// Creates an index for the specified table and columns. - /// - /// Name of the index to create - /// Name of the database table - /// An array of column names to index - /// Whether the index should be unique - public int CreateIndex(string indexName, string tableName, string[] columnNames, bool unique = false) - { - const string sqlFormat = "create {2} index if not exists \"{3}\" on \"{0}\"(\"{1}\")"; - var sql = String.Format(sqlFormat, tableName, string.Join ("\", \"", columnNames), unique ? "unique" : "", indexName); - return Execute(sql); - } - - /// - /// Creates an index for the specified table and column. - /// - /// Name of the index to create - /// Name of the database table - /// Name of the column to index - /// Whether the index should be unique - public int CreateIndex(string indexName, string tableName, string columnName, bool unique = false) - { - return CreateIndex(indexName, tableName, new string[] { columnName }, unique); - } - - /// - /// Creates an index for the specified table and column. - /// - /// Name of the database table - /// Name of the column to index - /// Whether the index should be unique - public int CreateIndex(string tableName, string columnName, bool unique = false) - { - return CreateIndex(tableName + "_" + columnName, tableName, columnName, unique); - } - - /// - /// Creates an index for the specified table and columns. - /// - /// Name of the database table - /// An array of column names to index - /// Whether the index should be unique - public int CreateIndex(string tableName, string[] columnNames, bool unique = false) - { - return CreateIndex(tableName + "_" + string.Join ("_", columnNames), tableName, columnNames, unique); - } - - /// - /// Creates an index for the specified object property. - /// e.g. CreateIndex(c => c.Name); - /// - /// Type to reflect to a database table. - /// Property to index - /// Whether the index should be unique - public void CreateIndex(Expression> property, bool unique = false) - { - MemberExpression mx; - if (property.Body.NodeType == ExpressionType.Convert) - { - mx = ((UnaryExpression)property.Body).Operand as MemberExpression; - } - else - { - mx= (property.Body as MemberExpression); - } - var propertyInfo = mx.Member as PropertyInfo; - if (propertyInfo == null) - { - throw new ArgumentException("The lambda expression 'property' should point to a valid Property"); - } - - var propName = propertyInfo.Name; - - var map = GetMapping(); - var colName = map.FindColumnWithPropertyName(propName).Name; - - CreateIndex(map.TableName, colName, unique); - } + /// + /// Creates an index for the specified object property. + /// e.g. CreateIndex<Client>(c => c.Name); + /// + /// Type to reflect to a database table. + /// Property to index + /// Whether the index should be unique + public int CreateIndex (Expression> property, bool unique = false) + { + MemberExpression mx; + if (property.Body.NodeType == ExpressionType.Convert) { + mx = ((UnaryExpression)property.Body).Operand as MemberExpression; + } + else { + mx = (property.Body as MemberExpression); + } + var propertyInfo = mx.Member as PropertyInfo; + if (propertyInfo == null) { + throw new ArgumentException ("The lambda expression 'property' should point to a valid Property"); + } + + var propName = propertyInfo.Name; + var map = GetMapping (); + var colName = map.FindColumnWithPropertyName (propName).Name; + + return CreateIndex (map.TableName, colName, unique); + } + + [Preserve (AllMembers = true)] public class ColumnInfo { -// public int cid { get; set; } + // public int cid { get; set; } [Column ("name")] public string Name { get; set; } -// [Column ("type")] -// public string ColumnType { get; set; } + // [Column ("type")] + // public string ColumnType { get; set; } public int notnull { get; set; } -// public string dflt_value { get; set; } + // public string dflt_value { get; set; } -// public int pk { get; set; } + // public int pk { get; set; } public override string ToString () { @@ -562,18 +749,21 @@ public override string ToString () } } + /// + /// Query the built-in sqlite table_info table for a specific tables columns. + /// + /// The columns contains in the table. + /// Table name. public List GetTableInfo (string tableName) { - var query = "pragma table_info(\"" + tableName + "\")"; + var query = "pragma table_info(\"" + tableName + "\")"; return Query (query); } - void MigrateTable (TableMapping map) + void MigrateTable (TableMapping map, List existingCols) { - var existingCols = GetTableInfo (map.TableName); - var toBeAdded = new List (); - + foreach (var p in map.Columns) { var found = false; foreach (var c in existingCols) { @@ -585,7 +775,7 @@ void MigrateTable (TableMapping map) toBeAdded.Add (p); } } - + foreach (var p in toBeAdded) { var addCol = "alter table \"" + map.TableName + "\" add column " + Orm.SqlDecl (p, StoreDateTimeAsTicks); Execute (addCol); @@ -608,7 +798,7 @@ protected virtual SQLiteCommand NewCommand () /// /// The fully escaped SQL. /// - /// + /// /// Arguments to substitute for the occurences of '?' in the command text. /// /// @@ -647,7 +837,7 @@ public SQLiteCommand CreateCommand (string cmdText, params object[] ps) public int Execute (string query, params object[] args) { var cmd = CreateCommand (query, args); - + if (TimeExecution) { if (_sw == null) { _sw = new Stopwatch (); @@ -657,20 +847,36 @@ public int Execute (string query, params object[] args) } var r = cmd.ExecuteNonQuery (); - + if (TimeExecution) { _sw.Stop (); _elapsedMilliseconds += _sw.ElapsedMilliseconds; - Debug.WriteLine (string.Format ("Finished in {0} ms ({1:0.0} s total)", _sw.ElapsedMilliseconds, _elapsedMilliseconds / 1000.0)); + Tracer?.Invoke (string.Format ("Finished in {0} ms ({1:0.0} s total)", _sw.ElapsedMilliseconds, _elapsedMilliseconds / 1000.0)); } - + return r; } + /// + /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' + /// in the command text for each of the arguments and then executes that command. + /// Use this method when return primitive values. + /// You can set the Trace or TimeExecution properties of the connection + /// to profile execution. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// The number of rows modified in the database as a result of this execution. + /// public T ExecuteScalar (string query, params object[] args) { var cmd = CreateCommand (query, args); - + if (TimeExecution) { if (_sw == null) { _sw = new Stopwatch (); @@ -678,15 +884,15 @@ public T ExecuteScalar (string query, params object[] args) _sw.Reset (); _sw.Start (); } - + var r = cmd.ExecuteScalar (); - + if (TimeExecution) { _sw.Stop (); _elapsedMilliseconds += _sw.ElapsedMilliseconds; - Debug.WriteLine (string.Format ("Finished in {0} ms ({1:0.0} s total)", _sw.ElapsedMilliseconds, _elapsedMilliseconds / 1000.0)); + Tracer?.Invoke (string.Format ("Finished in {0} ms ({1:0.0} s total)", _sw.ElapsedMilliseconds, _elapsedMilliseconds / 1000.0)); } - + return r; } @@ -725,13 +931,14 @@ public T ExecuteScalar (string query, params object[] args) /// /// /// An enumerable with one result for each row returned by the query. - /// The enumerator will call sqlite3_step on each call to MoveNext, so the database + /// The enumerator (retrieved by calling GetEnumerator() on the result of this method) + /// will call sqlite3_step on each call to MoveNext, so the database /// connection must remain open for the lifetime of the enumerator. /// - public IEnumerable DeferredQuery(string query, params object[] args) where T : new() + public IEnumerable DeferredQuery (string query, params object[] args) where T : new() { - var cmd = CreateCommand(query, args); - return cmd.ExecuteDeferredQuery(); + var cmd = CreateCommand (query, args); + return cmd.ExecuteDeferredQuery (); } /// @@ -779,13 +986,14 @@ public List Query (TableMapping map, string query, params object[] args) /// /// /// An enumerable with one result for each row returned by the query. - /// The enumerator will call sqlite3_step on each call to MoveNext, so the database + /// The enumerator (retrieved by calling GetEnumerator() on the result of this method) + /// will call sqlite3_step on each call to MoveNext, so the database /// connection must remain open for the lifetime of the enumerator. /// - public IEnumerable DeferredQuery(TableMapping map, string query, params object[] args) + public IEnumerable DeferredQuery (TableMapping map, string query, params object[] args) { - var cmd = CreateCommand(query, args); - return cmd.ExecuteDeferredQuery(map); + var cmd = CreateCommand (query, args); + return cmd.ExecuteDeferredQuery (map); } /// @@ -814,25 +1022,45 @@ public IEnumerable DeferredQuery(TableMapping map, string query, params /// public T Get (object pk) where T : new() { - var map = GetMapping (typeof(T)); + var map = GetMapping (typeof (T)); return Query (map.GetByPrimaryKeySql, pk).First (); } - /// - /// Attempts to retrieve the first object that matches the predicate from the table - /// associated with the specified type. - /// - /// - /// A predicate for which object to find. - /// - /// - /// The object that matches the given predicate. Throws a not found exception - /// if the object is not found. - /// - public T Get (Expression> predicate) where T : new() - { - return Table ().Where (predicate).First (); - } + /// + /// Attempts to retrieve an object with the given primary key from the table + /// associated with the specified type. Use of this method requires that + /// the given type have a designated PrimaryKey (using the PrimaryKeyAttribute). + /// + /// + /// The primary key. + /// + /// + /// The TableMapping used to identify the table. + /// + /// + /// The object with the given primary key. Throws a not found exception + /// if the object is not found. + /// + public object Get (object pk, TableMapping map) + { + return Query (map, map.GetByPrimaryKeySql, pk).First (); + } + + /// + /// Attempts to retrieve the first object that matches the predicate from the table + /// associated with the specified type. + /// + /// + /// A predicate for which object to find. + /// + /// + /// The object that matches the given predicate. Throws a not found exception + /// if the object is not found. + /// + public T Get (Expression> predicate) where T : new() + { + return Table ().Where (predicate).First (); + } /// /// Attempts to retrieve an object with the given primary key from the table @@ -846,7 +1074,7 @@ public IEnumerable DeferredQuery(TableMapping map, string query, params /// The object with the given primary key or null /// if the object is not found. /// - public T Find (object pk) where T : new () + public T Find (object pk) where T : new() { var map = GetMapping (typeof (T)); return Query (map.GetByPrimaryKeySql, pk).FirstOrDefault (); @@ -861,7 +1089,7 @@ public IEnumerable DeferredQuery(TableMapping map, string query, params /// The primary key. /// /// - /// The TableMapping used to identify the object type. + /// The TableMapping used to identify the table. /// /// /// The object with the given primary key or null @@ -871,26 +1099,26 @@ public object Find (object pk, TableMapping map) { return Query (map, map.GetByPrimaryKeySql, pk).FirstOrDefault (); } - + /// - /// Attempts to retrieve the first object that matches the predicate from the table - /// associated with the specified type. - /// - /// - /// A predicate for which object to find. - /// - /// - /// The object that matches the given predicate or null - /// if the object is not found. - /// - public T Find (Expression> predicate) where T : new() - { - return Table ().Where (predicate).FirstOrDefault (); - } + /// Attempts to retrieve the first object that matches the predicate from the table + /// associated with the specified type. + /// + /// + /// A predicate for which object to find. + /// + /// + /// The object that matches the given predicate or null + /// if the object is not found. + /// + public T Find (Expression> predicate) where T : new() + { + return Table ().Where (predicate).FirstOrDefault (); + } /// /// Attempts to retrieve the first object that matches the query from the table - /// associated with the specified type. + /// associated with the specified type. /// /// /// The fully escaped SQL. @@ -907,6 +1135,28 @@ public object Find (object pk, TableMapping map) return Query (query, args).FirstOrDefault (); } + /// + /// Attempts to retrieve the first object that matches the query from the table + /// associated with the specified type. + /// + /// + /// The TableMapping used to identify the table. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// The object that matches the given predicate or null + /// if the object is not found. + /// + public object FindWithQuery (TableMapping map, string query, params object[] args) + { + return Query (map, query, args).FirstOrDefault (); + } + /// /// Whether has been called and the database is waiting for a . /// @@ -920,39 +1170,42 @@ public bool IsInTransaction { /// Throws if a transaction has already begun. public void BeginTransaction () { - // The BEGIN command only works if the transaction stack is empty, - // or in other words if there are no pending transactions. - // If the transaction stack is not empty when the BEGIN command is invoked, + // The BEGIN command only works if the transaction stack is empty, + // or in other words if there are no pending transactions. + // If the transaction stack is not empty when the BEGIN command is invoked, // then the command fails with an error. // Rather than crash with an error, we will just ignore calls to BeginTransaction // that would result in an error. if (Interlocked.CompareExchange (ref _transactionDepth, 1, 0) == 0) { try { Execute ("begin transaction"); - } catch (Exception ex) { + } + catch (Exception ex) { var sqlExp = ex as SQLiteException; if (sqlExp != null) { - // It is recommended that applications respond to the errors listed below + // It is recommended that applications respond to the errors listed below // by explicitly issuing a ROLLBACK command. // TODO: This rollback failsafe should be localized to all throw sites. switch (sqlExp.Result) { - case SQLite3.Result.IOError: - case SQLite3.Result.Full: - case SQLite3.Result.Busy: - case SQLite3.Result.NoMem: - case SQLite3.Result.Interrupt: - RollbackTo (null, true); - break; + case SQLite3.Result.IOError: + case SQLite3.Result.Full: + case SQLite3.Result.Busy: + case SQLite3.Result.NoMem: + case SQLite3.Result.Interrupt: + RollbackTo (null, true); + break; } - } else { - // Call decrement and not VolatileWrite in case we've already + } + else { + // Call decrement and not VolatileWrite in case we've already // created a transaction point in SaveTransactionPoint since the catch. Interlocked.Decrement (ref _transactionDepth); } throw; } - } else { + } + else { // Calling BeginTransaction on an already open transaction is invalid throw new InvalidOperationException ("Cannot begin a transaction while already in a transaction."); } @@ -961,8 +1214,8 @@ public void BeginTransaction () /// /// Creates a savepoint in the database at the current point in the transaction timeline. /// Begins a new transaction if one is not in progress. - /// - /// Call to undo transactions since the returned savepoint. + /// + /// Call to undo transactions since the returned savepoint. /// Call to commit transactions after the savepoint returned here. /// Call to end the transaction, committing all changes. /// @@ -974,22 +1227,24 @@ public string SaveTransactionPoint () try { Execute ("savepoint " + retVal); - } catch (Exception ex) { + } + catch (Exception ex) { var sqlExp = ex as SQLiteException; if (sqlExp != null) { - // It is recommended that applications respond to the errors listed below + // It is recommended that applications respond to the errors listed below // by explicitly issuing a ROLLBACK command. // TODO: This rollback failsafe should be localized to all throw sites. switch (sqlExp.Result) { - case SQLite3.Result.IOError: - case SQLite3.Result.Full: - case SQLite3.Result.Busy: - case SQLite3.Result.NoMem: - case SQLite3.Result.Interrupt: - RollbackTo (null, true); - break; + case SQLite3.Result.IOError: + case SQLite3.Result.Full: + case SQLite3.Result.Busy: + case SQLite3.Result.NoMem: + case SQLite3.Result.Interrupt: + RollbackTo (null, true); + break; } - } else { + } + else { Interlocked.Decrement (ref _transactionDepth); } @@ -1019,38 +1274,58 @@ public void RollbackTo (string savepoint) /// /// Rolls back the transaction that was begun by . /// + /// The name of the savepoint to roll back to, as returned by . If savepoint is null or empty, this method is equivalent to a call to /// true to avoid throwing exceptions, false otherwise void RollbackTo (string savepoint, bool noThrow) { - // Rolling back without a TO clause rolls backs all transactions - // and leaves the transaction stack empty. + // Rolling back without a TO clause rolls backs all transactions + // and leaves the transaction stack empty. try { if (String.IsNullOrEmpty (savepoint)) { if (Interlocked.Exchange (ref _transactionDepth, 0) > 0) { Execute ("rollback"); } - } else { + } + else { DoSavePointExecute (savepoint, "rollback to "); - } - } catch (SQLiteException) { + } + } + catch (SQLiteException) { if (!noThrow) throw; - + } // No need to rollback if there are no transactions open. } /// - /// Releases a savepoint returned from . Releasing a savepoint + /// Releases a savepoint returned from . Releasing a savepoint /// makes changes since that savepoint permanent if the savepoint began the transaction, /// or otherwise the changes are permanent pending a call to . - /// + /// /// The RELEASE command is like a COMMIT for a SAVEPOINT. /// /// The name of the savepoint to release. The string should be the result of a call to public void Release (string savepoint) { - DoSavePointExecute (savepoint, "release "); + try { + DoSavePointExecute (savepoint, "release "); + } + catch (SQLiteException ex) { + if (ex.Result == SQLite3.Result.Busy) { + // Force a rollback since most people don't know this function can fail + // Don't call Rollback() since the _transactionDepth is 0 and it won't try + // Calling rollback makes our _transactionDepth variable correct. + // Writes to the database only happen at depth=0, so this failure will only happen then. + try { + Execute ("rollback"); + } + catch { + // rollback can fail in all sorts of wonderful version-dependent ways. Let's just hope for the best + } + } + throw; + } } void DoSavePointExecute (string savepoint, string cmd) @@ -1063,13 +1338,13 @@ void DoSavePointExecute (string savepoint, string cmd) // TODO: Mild race here, but inescapable without locking almost everywhere. if (0 <= depth && depth < _transactionDepth) { #if NETFX_CORE || USE_SQLITEPCL_RAW || NETCORE - Volatile.Write (ref _transactionDepth, depth); + Volatile.Write (ref _transactionDepth, depth); #elif SILVERLIGHT _transactionDepth = depth; #else Thread.VolatileWrite (ref _transactionDepth, depth); #endif - Execute (cmd + savepoint); + Execute (cmd + savepoint); return; } } @@ -1084,18 +1359,32 @@ void DoSavePointExecute (string savepoint, string cmd) public void Commit () { if (Interlocked.Exchange (ref _transactionDepth, 0) != 0) { - Execute ("commit"); + try { + Execute ("commit"); + } + catch { + // Force a rollback since most people don't know this function can fail + // Don't call Rollback() since the _transactionDepth is 0 and it won't try + // Calling rollback makes our _transactionDepth variable correct. + try { + Execute ("rollback"); + } + catch { + // rollback can fail in all sorts of wonderful version-dependent ways. Let's just hope for the best + } + throw; + } } // Do nothing on a commit with no open transaction } /// - /// Executes within a (possibly nested) transaction by wrapping it in a SAVEPOINT. If an + /// Executes within a (possibly nested) transaction by wrapping it in a SAVEPOINT. If an /// exception occurs the whole transaction is rolled back, not just the current savepoint. The exception /// is rethrown. /// /// - /// The to perform within a transaction. can contain any number + /// The to perform within a transaction. can contain any number /// of operations on the connection but should never call or /// . /// @@ -1105,7 +1394,8 @@ public void RunInTransaction (Action action) var savePoint = SaveTransactionPoint (); action (); Release (savePoint); - } catch (Exception) { + } + catch (Exception) { Rollback (); throw; } @@ -1122,11 +1412,11 @@ public void RunInTransaction (Action action) /// /// The number of rows added to the table. /// - public int InsertAll (System.Collections.IEnumerable objects, bool runInTransaction=true) + public int InsertAll (System.Collections.IEnumerable objects, bool runInTransaction = true) { var c = 0; if (runInTransaction) { - RunInTransaction(() => { + RunInTransaction (() => { foreach (var r in objects) { c += Insert (r); } @@ -1149,13 +1439,13 @@ public int InsertAll (System.Collections.IEnumerable objects, bool runInTransact /// /// Literal SQL code that gets placed into the command. INSERT {extra} INTO ... /// - /// + /// /// A boolean indicating if the inserts should be wrapped in a transaction. /// /// /// The number of rows added to the table. /// - public int InsertAll (System.Collections.IEnumerable objects, string extra, bool runInTransaction=true) + public int InsertAll (System.Collections.IEnumerable objects, string extra, bool runInTransaction = true) { var c = 0; if (runInTransaction) { @@ -1167,7 +1457,7 @@ public int InsertAll (System.Collections.IEnumerable objects, string extra, bool } else { foreach (var r in objects) { - c+= Insert (r); + c += Insert (r); } } return c; @@ -1182,13 +1472,13 @@ public int InsertAll (System.Collections.IEnumerable objects, string extra, bool /// /// The type of object to insert. /// - /// + /// /// A boolean indicating if the inserts should be wrapped in a transaction. /// /// /// The number of rows added to the table. /// - public int InsertAll (System.Collections.IEnumerable objects, Type objType, bool runInTransaction=true) + public int InsertAll (System.Collections.IEnumerable objects, Type objType, bool runInTransaction = true) { var c = 0; if (runInTransaction) { @@ -1205,10 +1495,11 @@ public int InsertAll (System.Collections.IEnumerable objects, Type objType, bool } return c; } - + /// - /// Inserts the given object and retrieves its - /// auto incremented primary key if it has one. + /// Inserts the given object (and updates its + /// auto incremented primary key if it has one). + /// The return value is the number of rows added to the table. /// /// /// The object to insert. @@ -1221,12 +1512,13 @@ public int Insert (object obj) if (obj == null) { return 0; } - return Insert (obj, "", obj.GetType ()); + return Insert (obj, "", Orm.GetType (obj)); } /// - /// Inserts the given object and retrieves its - /// auto incremented primary key if it has one. + /// Inserts the given object (and updates its + /// auto incremented primary key if it has one). + /// The return value is the number of rows added to the table. /// If a UNIQUE constraint violation occurs with /// some pre-existing object, this function deletes /// the old object. @@ -1242,12 +1534,13 @@ public int InsertOrReplace (object obj) if (obj == null) { return 0; } - return Insert (obj, "OR REPLACE", obj.GetType ()); + return Insert (obj, "OR REPLACE", Orm.GetType (obj)); } /// - /// Inserts the given object and retrieves its - /// auto incremented primary key if it has one. + /// Inserts the given object (and updates its + /// auto incremented primary key if it has one). + /// The return value is the number of rows added to the table. /// /// /// The object to insert. @@ -1264,8 +1557,9 @@ public int Insert (object obj, Type objType) } /// - /// Inserts the given object and retrieves its - /// auto incremented primary key if it has one. + /// Inserts the given object (and updates its + /// auto incremented primary key if it has one). + /// The return value is the number of rows added to the table. /// If a UNIQUE constraint violation occurs with /// some pre-existing object, this function deletes /// the old object. @@ -1283,10 +1577,11 @@ public int InsertOrReplace (object obj, Type objType) { return Insert (obj, "OR REPLACE", objType); } - + /// - /// Inserts the given object and retrieves its - /// auto incremented primary key if it has one. + /// Inserts the given object (and updates its + /// auto incremented primary key if it has one). + /// The return value is the number of rows added to the table. /// /// /// The object to insert. @@ -1302,76 +1597,49 @@ public int Insert (object obj, string extra) if (obj == null) { return 0; } - return Insert (obj, extra, obj.GetType ()); - } - - /// - /// Inserts the given object and retrieves its - /// auto incremented primary key if it has one. - /// - /// - /// The object to insert. - /// - /// - /// Literal SQL code that gets placed into the command. INSERT {extra} INTO ... - /// - /// - /// The type of object to insert. - /// - /// - /// The number of rows added to the table. - /// - public int Insert (object obj, string extra, Type objType) + return Insert (obj, extra, Orm.GetType (obj)); + } + + /// + /// Inserts the given object (and updates its + /// auto incremented primary key if it has one). + /// The return value is the number of rows added to the table. + /// + /// + /// The object to insert. + /// + /// + /// Literal SQL code that gets placed into the command. INSERT {extra} INTO ... + /// + /// + /// The type of object to insert. + /// + /// + /// The number of rows added to the table. + /// + public int Insert (object obj, string extra, Type objType) { if (obj == null || objType == null) { return 0; } - - - var map = GetMapping (objType); -#if USE_NEW_REFLECTION_API - if (map.PK != null && map.PK.IsAutoGuid) - { - // no GetProperty so search our way up the inheritance chain till we find it - PropertyInfo prop; - while (objType != null) - { - var info = objType.GetTypeInfo(); - prop = info.GetDeclaredProperty(map.PK.PropertyName); - if (prop != null) - { - if (prop.GetValue(obj, null).Equals(Guid.Empty)) - { - prop.SetValue(obj, Guid.NewGuid(), null); - } - break; - } - - objType = info.BaseType; - } - } -#else - if (map.PK != null && map.PK.IsAutoGuid) { - var prop = objType.GetProperty(map.PK.PropertyName); - if (prop != null) { - if (prop.GetValue(obj, null).Equals(Guid.Empty)) { - prop.SetValue(obj, Guid.NewGuid(), null); - } - } - } -#endif + var map = GetMapping (objType); + if (map.PK != null && map.PK.IsAutoGuid) { + if (map.PK.GetValue (obj).Equals (Guid.Empty)) { + map.PK.SetValue (obj, Guid.NewGuid ()); + } + } var replacing = string.Compare (extra, "OR REPLACE", StringComparison.OrdinalIgnoreCase) == 0; - + var cols = replacing ? map.InsertOrReplaceColumns : map.InsertColumns; var vals = new object[cols.Length]; for (var i = 0; i < vals.Length; i++) { - vals [i] = cols [i].GetValue (obj); + vals[i] = cols[i].GetValue (obj); } - - var insertCmd = map.GetInsertCommand (this, extra); + + var insertCmd = GetInsertCommand (map, extra); int count; lock (insertCmd) { @@ -1379,7 +1647,8 @@ public int Insert (object obj, string extra, Type objType) // A SQLite prepared statement can be bound for only one operation at a time. try { count = insertCmd.ExecuteNonQuery (vals); - } catch (SQLiteException ex) { + } + catch (SQLiteException ex) { if (SQLite3.ExtendedErrCode (this.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { throw NotNullConstraintViolationException.New (ex.Result, ex.Message, map, obj); } @@ -1397,6 +1666,61 @@ public int Insert (object obj, string extra, Type objType) return count; } + readonly Dictionary, PreparedSqlLiteInsertCommand> _insertCommandMap = new Dictionary, PreparedSqlLiteInsertCommand> (); + + PreparedSqlLiteInsertCommand GetInsertCommand (TableMapping map, string extra) + { + PreparedSqlLiteInsertCommand prepCmd; + + var key = Tuple.Create (map.MappedType.FullName, extra); + + lock (_insertCommandMap) { + _insertCommandMap.TryGetValue (key, out prepCmd); + } + + if (prepCmd == null) { + prepCmd = CreateInsertCommand (map, extra); + var added = false; + lock (_insertCommandMap) { + if (!_insertCommandMap.ContainsKey (key)) { + _insertCommandMap.Add (key, prepCmd); + added = true; + } + } + if (!added) { + prepCmd.Dispose (); + } + } + + return prepCmd; + } + + PreparedSqlLiteInsertCommand CreateInsertCommand (TableMapping map, string extra) + { + var cols = map.InsertColumns; + string insertSql; + if (cols.Length == 0 && map.Columns.Length == 1 && map.Columns[0].IsAutoInc) { + insertSql = string.Format ("insert {1} into \"{0}\" default values", map.TableName, extra); + } + else { + var replacing = string.Compare (extra, "OR REPLACE", StringComparison.OrdinalIgnoreCase) == 0; + + if (replacing) { + cols = map.InsertOrReplaceColumns; + } + + insertSql = string.Format ("insert {3} into \"{0}\"({1}) values ({2})", map.TableName, + string.Join (",", (from c in cols + select "\"" + c.Name + "\"").ToArray ()), + string.Join (",", (from c in cols + select "?").ToArray ()), extra); + + } + + var insertCommand = new PreparedSqlLiteInsertCommand (this, insertSql); + return insertCommand; + } + /// /// Updates all of the columns of a table using the specified object /// except for its primary key. @@ -1413,7 +1737,7 @@ public int Update (object obj) if (obj == null) { return 0; } - return Update (obj, obj.GetType ()); + return Update (obj, Orm.GetType (obj)); } /// @@ -1436,24 +1760,32 @@ public int Update (object obj, Type objType) if (obj == null || objType == null) { return 0; } - + var map = GetMapping (objType); - + var pk = map.PK; - + if (pk == null) { throw new NotSupportedException ("Cannot update " + map.TableName + ": it has no PK"); } - + var cols = from p in map.Columns - where p != pk - select p; + where p != pk + select p; var vals = from c in cols - select c.GetValue (obj); + select c.GetValue (obj); var ps = new List (vals); + if (ps.Count == 0) { + // There is a PK but no accompanying data, + // so reset the PK to make the UPDATE work. + cols = map.Columns; + vals = from c in cols + select c.GetValue (obj); + ps = new List (vals); + } ps.Add (pk.GetValue (obj)); var q = string.Format ("update \"{0}\" set {1} where {2} = ? ", map.TableName, string.Join (",", (from c in cols - select "\"" + c.Name + "\" = ? ").ToArray ()), pk.Name); + select "\"" + c.Name + "\" = ? ").ToArray ()), pk.Name); try { rowsAffected = Execute (q, ps.ToArray ()); @@ -1479,13 +1811,13 @@ public int Update (object obj, Type objType) /// /// An of the objects to insert. /// - /// + /// /// A boolean indicating if the inserts should be wrapped in a transaction /// /// /// The number of rows modified. /// - public int UpdateAll (System.Collections.IEnumerable objects, bool runInTransaction=true) + public int UpdateAll (System.Collections.IEnumerable objects, bool runInTransaction = true) { var c = 0; if (runInTransaction) { @@ -1514,7 +1846,7 @@ public int UpdateAll (System.Collections.IEnumerable objects, bool runInTransact /// public int Delete (object objectToDelete) { - var map = GetMapping (objectToDelete.GetType ()); + var map = GetMapping (Orm.GetType (objectToDelete)); var pk = map.PK; if (pk == null) { throw new NotSupportedException ("Cannot delete " + map.TableName + ": it has no PK"); @@ -1540,7 +1872,23 @@ public int Delete (object objectToDelete) /// public int Delete (object primaryKey) { - var map = GetMapping (typeof (T)); + return Delete (primaryKey, GetMapping (typeof (T))); + } + + /// + /// Deletes the object with the specified primary key. + /// + /// + /// The primary key of the object to delete. + /// + /// + /// The TableMapping used to identify the table. + /// + /// + /// The number of objects deleted. + /// + public int Delete (object primaryKey, TableMapping map) + { var pk = map.PK; if (pk == null) { throw new NotSupportedException ("Cannot delete " + map.TableName + ": it has no PK"); @@ -1566,7 +1914,23 @@ public int Delete (object primaryKey) public int DeleteAll () { var map = GetMapping (typeof (T)); - var query = string.Format("delete from \"{0}\"", map.TableName); + return DeleteAll (map); + } + + /// + /// Deletes all the objects from the specified table. + /// WARNING WARNING: Let me repeat. It deletes ALL the objects from the + /// specified table. Do you really want to do that? + /// + /// + /// The TableMapping used to identify the table. + /// + /// + /// The number of objects deleted. + /// + public int DeleteAll (TableMapping map) + { + var query = string.Format ("delete from \"{0}\"", map.TableName); var count = Execute (query); if (count > 0) OnTableChanged (map, NotifyTableChangedAction.Delete); @@ -1580,34 +1944,37 @@ public int DeleteAll () public void Dispose () { - Dispose(true); - GC.SuppressFinalize(this); + Dispose (true); + GC.SuppressFinalize (this); } - public void Close() + public void Close () { - Dispose(true); + Dispose (true); } - protected virtual void Dispose(bool disposing) + protected virtual void Dispose (bool disposing) { + var useClose2 = LibVersionNumber >= 3007014; + if (_open && Handle != NullHandle) { try { if (disposing) { - if (_mappings != null) { - foreach (var sqlInsertCommand in _mappings.Values){ - sqlInsertCommand.Dispose(); + lock (_insertCommandMap) { + foreach (var sqlInsertCommand in _insertCommandMap.Values) { + sqlInsertCommand.Dispose (); } + _insertCommandMap.Clear (); } - var r = SQLite3.Close(Handle); - if (r != SQLite3.Result.OK) - { - string msg = SQLite3.GetErrmsg(Handle); - throw SQLiteException.New(r, msg); + var r = useClose2 ? SQLite3.Close2 (Handle) : SQLite3.Close (Handle); + if (r != SQLite3.Result.OK) { + string msg = SQLite3.GetErrmsg (Handle); + throw SQLiteException.New (r, msg); } - } else { - SQLite3.Close2(Handle); + } + else { + var r = useClose2 ? SQLite3.Close2 (Handle) : SQLite3.Close (Handle); } } finally { @@ -1635,7 +2002,7 @@ public class NotifyTableChangedEventArgs : EventArgs public NotifyTableChangedEventArgs (TableMapping table, NotifyTableChangedAction action) { Table = table; - Action = action; + Action = action; } } @@ -1653,8 +2020,8 @@ public class SQLiteConnectionString { public string ConnectionString { get; private set; } public string DatabasePath { get; private set; } - public bool StoreDateTimeAsTicks { get; private set; } - + public bool StoreDateTimeAsTicks { get; private set; } + #if NETFX_CORE static readonly string MetroStyleDataPath = Windows.Storage.ApplicationData.Current.LocalFolder.Path; @@ -1670,28 +2037,35 @@ public static bool IsInMemoryPath(string databasePath) } #endif - - public SQLiteConnectionString (string databasePath, bool storeDateTimeAsTicks) + + public SQLiteConnectionString (string databasePath, bool storeDateTimeAsTicks) { ConnectionString = databasePath; - StoreDateTimeAsTicks = storeDateTimeAsTicks; - + StoreDateTimeAsTicks = storeDateTimeAsTicks; + #if NETFX_CORE DatabasePath = IsInMemoryPath(databasePath) ? databasePath : System.IO.Path.Combine(MetroStyleDataPath, databasePath); #else - DatabasePath = databasePath; + DatabasePath = databasePath; #endif } } - [AttributeUsage (AttributeTargets.Class)] + [AttributeUsage (AttributeTargets.Class)] public class TableAttribute : Attribute { public string Name { get; set; } + /// + /// Flag whether to create the table without rowid (see https://sqlite.org/withoutrowid.html) + /// + /// The default is false so that sqlite adds an implicit rowid to every table created. + /// + public bool WithoutRowId { get; set; } + public TableAttribute (string name) { Name = name; @@ -1725,12 +2099,12 @@ public class IndexedAttribute : Attribute public string Name { get; set; } public int Order { get; set; } public virtual bool Unique { get; set; } - - public IndexedAttribute() + + public IndexedAttribute () { } - - public IndexedAttribute(string name, int order) + + public IndexedAttribute (string name, int order) { Name = name; Order = order; @@ -1762,8 +2136,19 @@ public MaxLengthAttribute (int length) } } + public sealed class PreserveAttribute : System.Attribute + { + public bool AllMembers; + public bool Conditional; + } + + /// + /// Select the collating sequence to use on a column. + /// "BINARY", "NOCASE", and "RTRIM" are supported. + /// "BINARY" is the default. + /// [AttributeUsage (AttributeTargets.Property)] - public class CollationAttribute: Attribute + public class CollationAttribute : Attribute { public string Value { get; private set; } @@ -1776,57 +2161,73 @@ public CollationAttribute (string collation) [AttributeUsage (AttributeTargets.Property)] public class NotNullAttribute : Attribute { - } - - [AttributeUsage(AttributeTargets.Enum)] - public class StoreAsTextAttribute : Attribute - { - } - - public class TableMapping + } + + [AttributeUsage (AttributeTargets.Enum)] + public class StoreAsTextAttribute : Attribute + { + } + + public class TableMapping { public Type MappedType { get; private set; } public string TableName { get; private set; } + public bool WithoutRowId { get; private set; } + public Column[] Columns { get; private set; } public Column PK { get; private set; } public string GetByPrimaryKeySql { get; private set; } - Column _autoPk; - Column[] _insertColumns; - Column[] _insertOrReplaceColumns; + public CreateFlags CreateFlags { get; private set; } + + readonly Column _autoPk; + readonly Column[] _insertColumns; + readonly Column[] _insertOrReplaceColumns; - public TableMapping(Type type, CreateFlags createFlags = CreateFlags.None) + public TableMapping (Type type, CreateFlags createFlags = CreateFlags.None) { MappedType = type; + CreateFlags = createFlags; + + var typeInfo = type.GetTypeInfo (); + var tableAttr = + typeInfo.CustomAttributes + .Where (x => x.AttributeType == typeof (TableAttribute)) + .Select (x => (TableAttribute)Orm.InflateAttribute (x)) + .FirstOrDefault (); + + TableName = (tableAttr != null && !string.IsNullOrEmpty (tableAttr.Name)) ? tableAttr.Name : MappedType.Name; + WithoutRowId = tableAttr != null ? tableAttr.WithoutRowId : false; + + var props = new List (); + var baseType = type; + var propNames = new HashSet (); + while (baseType != typeof (object)) { + var ti = baseType.GetTypeInfo (); + var newProps = ( + from p in ti.DeclaredProperties + where + !propNames.Contains (p.Name) && + p.CanRead && p.CanWrite && + (p.GetMethod != null) && (p.SetMethod != null) && + (p.GetMethod.IsPublic && p.SetMethod.IsPublic) && + (!p.GetMethod.IsStatic) && (!p.SetMethod.IsStatic) + select p).ToList (); + foreach (var p in newProps) { + propNames.Add (p.Name); + } + props.AddRange (newProps); + baseType = ti.BaseType; + } -#if USE_NEW_REFLECTION_API - var tableAttr = (TableAttribute)System.Reflection.CustomAttributeExtensions - .GetCustomAttribute(type.GetTypeInfo(), typeof(TableAttribute), true); -#else - var tableAttr = (TableAttribute)type.GetCustomAttributes (typeof (TableAttribute), true).FirstOrDefault (); -#endif - - TableName = tableAttr != null ? tableAttr.Name : MappedType.Name; - -#if !USE_NEW_REFLECTION_API - var props = MappedType.GetProperties (BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty); -#else - var props = from p in MappedType.GetRuntimeProperties() - where ((p.GetMethod != null && p.GetMethod.IsPublic) || (p.SetMethod != null && p.SetMethod.IsPublic) || (p.GetMethod != null && p.GetMethod.IsStatic) || (p.SetMethod != null && p.SetMethod.IsStatic)) - select p; -#endif var cols = new List (); foreach (var p in props) { -#if !USE_NEW_REFLECTION_API - var ignore = p.GetCustomAttributes (typeof(IgnoreAttribute), true).Length > 0; -#else - var ignore = p.GetCustomAttributes (typeof(IgnoreAttribute), true).Count() > 0; -#endif - if (p.CanWrite && !ignore) { + var ignore = p.IsDefined (typeof (IgnoreAttribute), true); + if (!ignore) { cols.Add (new Column (p, createFlags)); } } @@ -1839,7 +2240,7 @@ public TableMapping(Type type, CreateFlags createFlags = CreateFlags.None) PK = c; } } - + HasAutoIncPK = _autoPk != null; if (PK != null) { @@ -1849,7 +2250,9 @@ public TableMapping(Type type, CreateFlags createFlags = CreateFlags.None) // People should not be calling Get/Find without a PK GetByPrimaryKeySql = string.Format ("select * from \"{0}\" limit 1", TableName); } - _insertCommandMap = new ConcurrentStringDictionary (); + + _insertColumns = Columns.Where (c => !c.IsAutoInc).ToArray (); + _insertOrReplaceColumns = Columns.ToArray (); } public bool HasAutoIncPK { get; private set; } @@ -1863,18 +2266,12 @@ public void SetAutoIncPK (object obj, long id) public Column[] InsertColumns { get { - if (_insertColumns == null) { - _insertColumns = Columns.Where (c => !c.IsAutoInc).ToArray (); - } return _insertColumns; } } public Column[] InsertOrReplaceColumns { get { - if (_insertOrReplaceColumns == null) { - _insertOrReplaceColumns = Columns.ToArray (); - } return _insertOrReplaceColumns; } } @@ -1887,79 +2284,26 @@ public Column FindColumnWithPropertyName (string propertyName) public Column FindColumn (string columnName) { - var exact = Columns.FirstOrDefault (c => c.Name.ToLower() == columnName.ToLower()); + var exact = Columns.FirstOrDefault (c => c.Name.ToLower () == columnName.ToLower ()); return exact; } - ConcurrentStringDictionary _insertCommandMap; - - public PreparedSqlLiteInsertCommand GetInsertCommand(SQLiteConnection conn, string extra) - { - object prepCmdO; - - if (!_insertCommandMap.TryGetValue (extra, out prepCmdO)) { - var prepCmd = CreateInsertCommand (conn, extra); - prepCmdO = prepCmd; - if (!_insertCommandMap.TryAdd (extra, prepCmd)) { - // Concurrent add attempt beat us. - prepCmd.Dispose (); - _insertCommandMap.TryGetValue (extra, out prepCmdO); - } - } - return (PreparedSqlLiteInsertCommand)prepCmdO; - } - - PreparedSqlLiteInsertCommand CreateInsertCommand(SQLiteConnection conn, string extra) - { - var cols = InsertColumns; - string insertSql; - if (!cols.Any() && Columns.Count() == 1 && Columns[0].IsAutoInc) - { - insertSql = string.Format("insert {1} into \"{0}\" default values", TableName, extra); - } - else - { - var replacing = string.Compare (extra, "OR REPLACE", StringComparison.OrdinalIgnoreCase) == 0; - - if (replacing) { - cols = InsertOrReplaceColumns; - } - - insertSql = string.Format("insert {3} into \"{0}\"({1}) values ({2})", TableName, - string.Join(",", (from c in cols - select "\"" + c.Name + "\"").ToArray()), - string.Join(",", (from c in cols - select "?").ToArray()), extra); - - } - - var insertCommand = new PreparedSqlLiteInsertCommand(conn); - insertCommand.CommandText = insertSql; - return insertCommand; - } - - protected internal void Dispose() - { - foreach (var pair in _insertCommandMap) { - ((PreparedSqlLiteInsertCommand)pair.Value).Dispose (); - } - _insertCommandMap = null; - } - public class Column { PropertyInfo _prop; public string Name { get; private set; } + public PropertyInfo PropertyInfo => _prop; + public string PropertyName { get { return _prop.Name; } } public Type ColumnType { get; private set; } public string Collation { get; private set; } - public bool IsAutoInc { get; private set; } - public bool IsAutoGuid { get; private set; } + public bool IsAutoInc { get; private set; } + public bool IsAutoGuid { get; private set; } public bool IsPK { get; private set; } @@ -1969,44 +2313,50 @@ public class Column public int? MaxStringLength { get; private set; } - public bool StoreAsText { get; private set; } + public bool StoreAsText { get; private set; } - public Column(PropertyInfo prop, CreateFlags createFlags = CreateFlags.None) - { - var colAttr = (ColumnAttribute)prop.GetCustomAttributes(typeof(ColumnAttribute), true).FirstOrDefault(); + public Column (PropertyInfo prop, CreateFlags createFlags = CreateFlags.None) + { + var colAttr = prop.CustomAttributes.FirstOrDefault (x => x.AttributeType == typeof (ColumnAttribute)); - _prop = prop; - Name = colAttr == null ? prop.Name : colAttr.Name; - //If this type is Nullable then Nullable.GetUnderlyingType returns the T, otherwise it returns null, so get the actual type instead - ColumnType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType; - Collation = Orm.Collation(prop); + _prop = prop; + Name = (colAttr != null && colAttr.ConstructorArguments.Count > 0) ? + colAttr.ConstructorArguments[0].Value?.ToString () : + prop.Name; + //If this type is Nullable then Nullable.GetUnderlyingType returns the T, otherwise it returns null, so get the actual type instead + ColumnType = Nullable.GetUnderlyingType (prop.PropertyType) ?? prop.PropertyType; + Collation = Orm.Collation (prop); - IsPK = Orm.IsPK(prop) || + IsPK = Orm.IsPK (prop) || (((createFlags & CreateFlags.ImplicitPK) == CreateFlags.ImplicitPK) && string.Compare (prop.Name, Orm.ImplicitPkName, StringComparison.OrdinalIgnoreCase) == 0); - var isAuto = Orm.IsAutoInc(prop) || (IsPK && ((createFlags & CreateFlags.AutoIncPK) == CreateFlags.AutoIncPK)); - IsAutoGuid = isAuto && ColumnType == typeof(Guid); - IsAutoInc = isAuto && !IsAutoGuid; - - Indices = Orm.GetIndices(prop); - if (!Indices.Any() - && !IsPK - && ((createFlags & CreateFlags.ImplicitIndex) == CreateFlags.ImplicitIndex) - && Name.EndsWith (Orm.ImplicitIndexSuffix, StringComparison.OrdinalIgnoreCase) - ) - { - Indices = new IndexedAttribute[] { new IndexedAttribute() }; - } - IsNullable = !(IsPK || Orm.IsMarkedNotNull(prop)); - MaxStringLength = Orm.MaxStringLength(prop); + var isAuto = Orm.IsAutoInc (prop) || (IsPK && ((createFlags & CreateFlags.AutoIncPK) == CreateFlags.AutoIncPK)); + IsAutoGuid = isAuto && ColumnType == typeof (Guid); + IsAutoInc = isAuto && !IsAutoGuid; + + Indices = Orm.GetIndices (prop); + if (!Indices.Any () + && !IsPK + && ((createFlags & CreateFlags.ImplicitIndex) == CreateFlags.ImplicitIndex) + && Name.EndsWith (Orm.ImplicitIndexSuffix, StringComparison.OrdinalIgnoreCase) + ) { + Indices = new IndexedAttribute[] { new IndexedAttribute () }; + } + IsNullable = !(IsPK || Orm.IsMarkedNotNull (prop)); + MaxStringLength = Orm.MaxStringLength (prop); - StoreAsText = prop.PropertyType.GetTypeInfo().GetCustomAttribute(typeof(StoreAsTextAttribute), false) != null; - } + StoreAsText = prop.PropertyType.GetTypeInfo ().CustomAttributes.Any (x => x.AttributeType == typeof (StoreAsTextAttribute)); + } public void SetValue (object obj, object val) { - _prop.SetValue (obj, val, null); + if (val != null && ColumnType.GetTypeInfo ().IsEnum) { + _prop.SetValue (obj, Enum.ToObject (ColumnType, val)); + } + else { + _prop.SetValue (obj, val, null); + } } public object GetValue (object obj) @@ -2014,74 +2364,78 @@ public object GetValue (object obj) return _prop.GetValue (obj, null); } } - } - - internal class EnumCacheInfo - { - public EnumCacheInfo(Type type) - { -#if !USE_NEW_REFLECTION_API - IsEnum = type.IsEnum; -#else - IsEnum = type.GetTypeInfo().IsEnum; -#endif - - if (IsEnum) - { - // This is a big assumption, but for now support ints only as key, otherwise we still - // have to pay the price for boxing - EnumValues = Enum.GetValues(type).Cast().ToDictionary(x => x, x => x.ToString()); - -#if !USE_NEW_REFLECTION_API - StoreAsText = type.GetCustomAttribute(typeof(StoreAsTextAttribute), false) != null; -#else - StoreAsText = type.GetTypeInfo().GetCustomAttribute(typeof(StoreAsTextAttribute), false) != null; -#endif - } - } - - public bool IsEnum { get; private set; } - - public bool StoreAsText { get; private set; } - - public Dictionary EnumValues { get; private set; } - } + } - internal static class EnumCache - { - private static readonly Dictionary Cache = new Dictionary(); + class EnumCacheInfo + { + public EnumCacheInfo (Type type) + { + var typeInfo = type.GetTypeInfo (); - public static EnumCacheInfo GetInfo() - { - return GetInfo(typeof(T)); - } + IsEnum = typeInfo.IsEnum; - public static EnumCacheInfo GetInfo(Type type) - { - lock (Cache) - { - EnumCacheInfo info = null; - if (!Cache.TryGetValue(type, out info)) - { - info = new EnumCacheInfo(type); - Cache[type] = info; - } + if (IsEnum) { + StoreAsText = typeInfo.CustomAttributes.Any (x => x.AttributeType == typeof (StoreAsTextAttribute)); + + if (StoreAsText) { + EnumValues = new Dictionary (); + foreach (object e in Enum.GetValues (type)) { + EnumValues[Convert.ToInt32 (e)] = e.ToString (); + } + } + } + } + + public bool IsEnum { get; private set; } + + public bool StoreAsText { get; private set; } + + public Dictionary EnumValues { get; private set; } + } + + static class EnumCache + { + static readonly Dictionary Cache = new Dictionary (); + + public static EnumCacheInfo GetInfo () + { + return GetInfo (typeof (T)); + } + + public static EnumCacheInfo GetInfo (Type type) + { + lock (Cache) { + EnumCacheInfo info = null; + if (!Cache.TryGetValue (type, out info)) { + info = new EnumCacheInfo (type); + Cache[type] = info; + } - return info; - } - } - } + return info; + } + } + } public static class Orm { - public const int DefaultMaxStringLength = 140; - public const string ImplicitPkName = "Id"; - public const string ImplicitIndexSuffix = "Id"; + public const int DefaultMaxStringLength = 140; + public const string ImplicitPkName = "Id"; + public const string ImplicitIndexSuffix = "Id"; + + public static Type GetType (object obj) + { + if (obj == null) + return typeof (object); + var rt = obj as IReflectableType; + if (rt != null) + return rt.GetTypeInfo ().AsType (); + return obj.GetType (); + } public static string SqlDecl (TableMapping.Column p, bool storeDateTimeAsTicks) { string decl = "\"" + p.Name + "\" " + SqlType (p, storeDateTimeAsTicks) + " "; - + if (p.IsPK) { decl += "primary key "; } @@ -2094,112 +2448,130 @@ public static string SqlDecl (TableMapping.Column p, bool storeDateTimeAsTicks) if (!string.IsNullOrEmpty (p.Collation)) { decl += "collate " + p.Collation + " "; } - + return decl; } public static string SqlType (TableMapping.Column p, bool storeDateTimeAsTicks) { var clrType = p.ColumnType; - if (clrType == typeof(Boolean) || clrType == typeof(Byte) || clrType == typeof(UInt16) || clrType == typeof(SByte) || clrType == typeof(Int16) || clrType == typeof(Int32) || clrType == typeof(UInt32) || clrType == typeof(Int64)) - { + if (clrType == typeof (Boolean) || clrType == typeof (Byte) || clrType == typeof (UInt16) || clrType == typeof (SByte) || clrType == typeof (Int16) || clrType == typeof (Int32) || clrType == typeof (UInt32) || clrType == typeof (Int64)) { return "integer"; - } else if (clrType == typeof(Single) || clrType == typeof(Double) || clrType == typeof(Decimal)) { + } + else if (clrType == typeof (Single) || clrType == typeof (Double) || clrType == typeof (Decimal)) { return "float"; - } else if (clrType == typeof(String)) { + } + else if (clrType == typeof (String) || clrType == typeof (StringBuilder) || clrType == typeof (Uri) || clrType == typeof (UriBuilder)) { int? len = p.MaxStringLength; if (len.HasValue) return "varchar(" + len.Value + ")"; return "varchar"; - } else if (clrType == typeof(TimeSpan)) { - return "bigint"; - } else if (clrType == typeof(DateTime)) { + } + else if (clrType == typeof (TimeSpan)) { + return "bigint"; + } + else if (clrType == typeof (DateTime)) { return storeDateTimeAsTicks ? "bigint" : "datetime"; - } else if (clrType == typeof(DateTimeOffset)) { + } + else if (clrType == typeof (DateTimeOffset)) { return "bigint"; -#if !USE_NEW_REFLECTION_API - } else if (clrType.IsEnum) { -#else - } else if (clrType.GetTypeInfo().IsEnum) { -#endif - if (p.StoreAsText) - return "varchar"; - else - return "integer"; - } else if (clrType == typeof(byte[])) { + } + else if (clrType.GetTypeInfo ().IsEnum) { + if (p.StoreAsText) + return "varchar"; + else + return "integer"; + } + else if (clrType == typeof (byte[])) { return "blob"; - } else if (clrType == typeof(Guid)) { - return "varchar(36)"; - } else { + } + else if (clrType == typeof (Guid)) { + return "varchar(36)"; + } + else { throw new NotSupportedException ("Don't know about " + clrType); } } public static bool IsPK (MemberInfo p) { - var attrs = p.GetCustomAttributes (typeof(PrimaryKeyAttribute), true); -#if !USE_NEW_REFLECTION_API - return attrs.Length > 0; -#else - return attrs.Count() > 0; -#endif + return p.CustomAttributes.Any (x => x.AttributeType == typeof (PrimaryKeyAttribute)); } public static string Collation (MemberInfo p) { - var attrs = p.GetCustomAttributes (typeof(CollationAttribute), true); -#if !USE_NEW_REFLECTION_API - if (attrs.Length > 0) { - return ((CollationAttribute)attrs [0]).Value; -#else - if (attrs.Count() > 0) { - return ((CollationAttribute)attrs.First()).Value; -#endif - } else { - return string.Empty; + return + (p.CustomAttributes + .Where (x => typeof (CollationAttribute) == x.AttributeType) + .Select (x => { + var args = x.ConstructorArguments; + return args.Count > 0 ? ((args[0].Value as string) ?? "") : ""; + }) + .FirstOrDefault ()) ?? ""; + } + + public static bool IsAutoInc (MemberInfo p) + { + return p.CustomAttributes.Any (x => x.AttributeType == typeof (AutoIncrementAttribute)); + } + + public static FieldInfo GetField (TypeInfo t, string name) + { + var f = t.GetDeclaredField (name); + if (f != null) + return f; + return GetField (t.BaseType.GetTypeInfo (), name); + } + + public static PropertyInfo GetProperty (TypeInfo t, string name) + { + var f = t.GetDeclaredProperty (name); + if (f != null) + return f; + return GetProperty (t.BaseType.GetTypeInfo (), name); + } + + public static object InflateAttribute (CustomAttributeData x) + { + var atype = x.AttributeType; + var typeInfo = atype.GetTypeInfo (); + var args = x.ConstructorArguments.Select (a => a.Value).ToArray (); + var r = Activator.CreateInstance (x.AttributeType, args); + foreach (var arg in x.NamedArguments) { + if (arg.IsField) { + GetField (typeInfo, arg.MemberName).SetValue (r, arg.TypedValue.Value); + } + else { + GetProperty (typeInfo, arg.MemberName).SetValue (r, arg.TypedValue.Value); + } } + return r; } - public static bool IsAutoInc (MemberInfo p) + public static IEnumerable GetIndices (MemberInfo p) { - var attrs = p.GetCustomAttributes (typeof(AutoIncrementAttribute), true); -#if !USE_NEW_REFLECTION_API - return attrs.Length > 0; -#else - return attrs.Count() > 0; -#endif + var indexedInfo = typeof (IndexedAttribute).GetTypeInfo (); + return + p.CustomAttributes + .Where (x => indexedInfo.IsAssignableFrom (x.AttributeType.GetTypeInfo ())) + .Select (x => (IndexedAttribute)InflateAttribute (x)); } - public static IEnumerable GetIndices(MemberInfo p) - { - var attrs = p.GetCustomAttributes(typeof(IndexedAttribute), true); - return attrs.Cast(); - } - - public static int? MaxStringLength(PropertyInfo p) + public static int? MaxStringLength (PropertyInfo p) { - var attrs = p.GetCustomAttributes (typeof(MaxLengthAttribute), true); -#if !USE_NEW_REFLECTION_API - if (attrs.Length > 0) - return ((MaxLengthAttribute)attrs [0]).Value; -#else - if (attrs.Count() > 0) - return ((MaxLengthAttribute)attrs.First()).Value; -#endif - + var attr = p.CustomAttributes.FirstOrDefault (x => x.AttributeType == typeof (MaxLengthAttribute)); + if (attr != null) { + var attrv = (MaxLengthAttribute)InflateAttribute (attr); + return attrv.Value; + } return null; } - public static bool IsMarkedNotNull(MemberInfo p) + public static bool IsMarkedNotNull (MemberInfo p) { - var attrs = p.GetCustomAttributes (typeof (NotNullAttribute), true); -#if !USE_NEW_REFLECTION_API - return attrs.Length > 0; -#else - return attrs.Count() > 0; -#endif + return p.CustomAttributes.Any (x => x.AttributeType == typeof (NotNullAttribute)); } } @@ -2220,9 +2592,9 @@ internal SQLiteCommand (SQLiteConnection conn) public int ExecuteNonQuery () { if (_conn.Trace) { - Debug.WriteLine ("Executing: " + this); + _conn.Tracer?.Invoke ("Executing: " + this); } - + var r = SQLite3.Result.OK; var stmt = Prepare (); r = SQLite3.Step (stmt); @@ -2230,7 +2602,8 @@ public int ExecuteNonQuery () if (r == SQLite3.Result.Done) { int rowsAffected = SQLite3.Changes (_conn.Handle); return rowsAffected; - } else if (r == SQLite3.Result.Error) { + } + else if (r == SQLite3.Result.Error) { string msg = SQLite3.GetErrmsg (_conn.Handle); throw SQLiteException.New (r, msg); } @@ -2240,22 +2613,22 @@ public int ExecuteNonQuery () } } - throw SQLiteException.New(r, r.ToString()); + throw SQLiteException.New (r, r.ToString ()); } public IEnumerable ExecuteDeferredQuery () { - return ExecuteDeferredQuery(_conn.GetMapping(typeof(T))); + return ExecuteDeferredQuery (_conn.GetMapping (typeof (T))); } public List ExecuteQuery () { - return ExecuteDeferredQuery(_conn.GetMapping(typeof(T))).ToList(); + return ExecuteDeferredQuery (_conn.GetMapping (typeof (T))).ToList (); } public List ExecuteQuery (TableMapping map) { - return ExecuteDeferredQuery(map).ToList(); + return ExecuteDeferredQuery (map).ToList (); } /// @@ -2278,67 +2651,62 @@ protected virtual void OnInstanceCreated (object obj) public IEnumerable ExecuteDeferredQuery (TableMapping map) { if (_conn.Trace) { - Debug.WriteLine ("Executing Query: " + this); + _conn.Tracer?.Invoke ("Executing Query: " + this); } var stmt = Prepare (); - try - { + try { var cols = new TableMapping.Column[SQLite3.ColumnCount (stmt)]; for (int i = 0; i < cols.Length; i++) { var name = SQLite3.ColumnName16 (stmt, i); - cols [i] = map.FindColumn (name); + cols[i] = map.FindColumn (name); } - + while (SQLite3.Step (stmt) == SQLite3.Result.Row) { - var obj = Activator.CreateInstance(map.MappedType); + var obj = Activator.CreateInstance (map.MappedType); for (int i = 0; i < cols.Length; i++) { - if (cols [i] == null) + if (cols[i] == null) continue; var colType = SQLite3.ColumnType (stmt, i); - var val = ReadCol (stmt, i, colType, cols [i].ColumnType); - cols [i].SetValue (obj, val); - } + var val = ReadCol (stmt, i, colType, cols[i].ColumnType); + cols[i].SetValue (obj, val); + } OnInstanceCreated (obj); yield return (T)obj; } } - finally - { - SQLite3.Finalize(stmt); + finally { + SQLite3.Finalize (stmt); } } public T ExecuteScalar () { if (_conn.Trace) { - Debug.WriteLine ("Executing Query: " + this); + _conn.Tracer?.Invoke ("Executing Query: " + this); } - - T val = default(T); - + + T val = default (T); + var stmt = Prepare (); - try - { - var r = SQLite3.Step (stmt); - if (r == SQLite3.Result.Row) { - var colType = SQLite3.ColumnType (stmt, 0); - val = (T)ReadCol (stmt, 0, colType, typeof(T)); - } - else if (r == SQLite3.Result.Done) { - } - else - { - throw SQLiteException.New (r, SQLite3.GetErrmsg (_conn.Handle)); - } - } - finally - { - Finalize (stmt); - } - + try { + var r = SQLite3.Step (stmt); + if (r == SQLite3.Result.Row) { + var colType = SQLite3.ColumnType (stmt, 0); + val = (T)ReadCol (stmt, 0, colType, typeof (T)); + } + else if (r == SQLite3.Result.Done) { + } + else { + throw SQLiteException.New (r, SQLite3.GetErrmsg (_conn.Handle)); + } + } + finally { + Finalize (stmt); + } + return val; } @@ -2358,16 +2726,16 @@ public void Bind (object val) public override string ToString () { var parts = new string[1 + _bindings.Count]; - parts [0] = CommandText; + parts[0] = CommandText; var i = 1; foreach (var b in _bindings) { - parts [i] = string.Format (" {0}: {1}", i - 1, b.Value); + parts[i] = string.Format (" {0}: {1}", i - 1, b.Value); i++; } return string.Join (Environment.NewLine, parts); } - Sqlite3Statement Prepare() + Sqlite3Statement Prepare () { var stmt = SQLite3.Prepare2 (_conn.Handle, CommandText); BindAll (stmt); @@ -2385,10 +2753,11 @@ void BindAll (Sqlite3Statement stmt) foreach (var b in _bindings) { if (b.Name != null) { b.Index = SQLite3.BindParameterIndex (stmt, b.Name); - } else { + } + else { b.Index = nextIdx++; } - + BindParameter (stmt, b.Index, b.Value, _conn.StoreDateTimeAsTicks); } } @@ -2401,47 +2770,69 @@ internal static void BindParameter (Sqlite3Statement stmt, int index, object val { if (value == null) { SQLite3.BindNull (stmt, index); - } else { + } + else { if (value is Int32) { SQLite3.BindInt (stmt, index, (int)value); - } else if (value is String) { + } + else if (value is String) { SQLite3.BindText (stmt, index, (string)value, -1, NegativePointer); - } else if (value is Byte || value is UInt16 || value is SByte || value is Int16) { + } + else if (value is Byte || value is UInt16 || value is SByte || value is Int16) { SQLite3.BindInt (stmt, index, Convert.ToInt32 (value)); - } else if (value is Boolean) { + } + else if (value is Boolean) { SQLite3.BindInt (stmt, index, (bool)value ? 1 : 0); - } else if (value is UInt32 || value is Int64) { + } + else if (value is UInt32 || value is Int64) { SQLite3.BindInt64 (stmt, index, Convert.ToInt64 (value)); - } else if (value is Single || value is Double || value is Decimal) { + } + else if (value is Single || value is Double || value is Decimal) { SQLite3.BindDouble (stmt, index, Convert.ToDouble (value)); - } else if (value is TimeSpan) { - SQLite3.BindInt64(stmt, index, ((TimeSpan)value).Ticks); - } else if (value is DateTime) { + } + else if (value is TimeSpan) { + SQLite3.BindInt64 (stmt, index, ((TimeSpan)value).Ticks); + } + else if (value is DateTime) { if (storeDateTimeAsTicks) { SQLite3.BindInt64 (stmt, index, ((DateTime)value).Ticks); } else { SQLite3.BindText (stmt, index, ((DateTime)value).ToString (DateTimeExactStoreFormat, System.Globalization.CultureInfo.InvariantCulture), -1, NegativePointer); } - } else if (value is DateTimeOffset) { + } + else if (value is DateTimeOffset) { SQLite3.BindInt64 (stmt, index, ((DateTimeOffset)value).UtcTicks); - } else { - // Now we could possibly get an enum, retrieve cached info - var valueType = value.GetType(); - var enumInfo = EnumCache.GetInfo(valueType); - if (enumInfo.IsEnum) { - var enumIntValue = Convert.ToInt32(value); - if (enumInfo.StoreAsText) - SQLite3.BindText(stmt, index, enumInfo.EnumValues[enumIntValue], -1, NegativePointer); - else - SQLite3.BindInt(stmt, index, enumIntValue); - } else if (value is byte[]){ - SQLite3.BindBlob(stmt, index, (byte[]) value, ((byte[]) value).Length, NegativePointer); - } else if (value is Guid) { - SQLite3.BindText(stmt, index, ((Guid)value).ToString(), 72, NegativePointer); - } else { - throw new NotSupportedException("Cannot store type: " + value.GetType()); - } + } + else if (value is byte[]) { + SQLite3.BindBlob (stmt, index, (byte[])value, ((byte[])value).Length, NegativePointer); + } + else if (value is Guid) { + SQLite3.BindText (stmt, index, ((Guid)value).ToString (), 72, NegativePointer); + } + else if (value is Uri) { + SQLite3.BindText (stmt, index, ((Uri)value).ToString (), -1, NegativePointer); + } + else if (value is StringBuilder) { + SQLite3.BindText (stmt, index, ((StringBuilder)value).ToString (), -1, NegativePointer); + } + else if (value is UriBuilder) { + SQLite3.BindText (stmt, index, ((UriBuilder)value).ToString (), -1, NegativePointer); + } + else { + // Now we could possibly get an enum, retrieve cached info + var valueType = value.GetType (); + var enumInfo = EnumCache.GetInfo (valueType); + if (enumInfo.IsEnum) { + var enumIntValue = Convert.ToInt32 (value); + if (enumInfo.StoreAsText) + SQLite3.BindText (stmt, index, enumInfo.EnumValues[enumIntValue], -1, NegativePointer); + else + SQLite3.BindInt (stmt, index, enumIntValue); + } + else { + throw new NotSupportedException ("Cannot store type: " + Orm.GetType (value)); + } } } } @@ -2459,20 +2850,28 @@ object ReadCol (Sqlite3Statement stmt, int index, SQLite3.ColType type, Type clr { if (type == SQLite3.ColType.Null) { return null; - } else { - if (clrType == typeof(String)) { + } + else { + var clrTypeInfo = clrType.GetTypeInfo (); + if (clrType == typeof (String)) { return SQLite3.ColumnString (stmt, index); - } else if (clrType == typeof(Int32)) { + } + else if (clrType == typeof (Int32)) { return (int)SQLite3.ColumnInt (stmt, index); - } else if (clrType == typeof(Boolean)) { + } + else if (clrType == typeof (Boolean)) { return SQLite3.ColumnInt (stmt, index) == 1; - } else if (clrType == typeof(double)) { + } + else if (clrType == typeof (double)) { return SQLite3.ColumnDouble (stmt, index); - } else if (clrType == typeof(float)) { + } + else if (clrType == typeof (float)) { return (float)SQLite3.ColumnDouble (stmt, index); - } else if (clrType == typeof(TimeSpan)) { - return new TimeSpan(SQLite3.ColumnInt64(stmt, index)); - } else if (clrType == typeof(DateTime)) { + } + else if (clrType == typeof (TimeSpan)) { + return new TimeSpan (SQLite3.ColumnInt64 (stmt, index)); + } + else if (clrType == typeof (DateTime)) { if (_conn.StoreDateTimeAsTicks) { return new DateTime (SQLite3.ColumnInt64 (stmt, index)); } @@ -2484,40 +2883,59 @@ object ReadCol (Sqlite3Statement stmt, int index, SQLite3.ColType type, Type clr } return resultDate; } - } else if (clrType == typeof(DateTimeOffset)) { - return new DateTimeOffset(SQLite3.ColumnInt64 (stmt, index),TimeSpan.Zero); -#if !USE_NEW_REFLECTION_API - } else if (clrType.IsEnum) { -#else - } else if (clrType.GetTypeInfo().IsEnum) { -#endif - if (type == SQLite3.ColType.Text) - { - var value = SQLite3.ColumnString(stmt, index); - return Enum.Parse(clrType, value.ToString(), true); - } - else - return SQLite3.ColumnInt (stmt, index); - } else if (clrType == typeof(Int64)) { + } + else if (clrType == typeof (DateTimeOffset)) { + return new DateTimeOffset (SQLite3.ColumnInt64 (stmt, index), TimeSpan.Zero); + } + else if (clrTypeInfo.IsEnum) { + if (type == SQLite3.ColType.Text) { + var value = SQLite3.ColumnString (stmt, index); + return Enum.Parse (clrType, value.ToString (), true); + } + else + return SQLite3.ColumnInt (stmt, index); + } + else if (clrType == typeof (Int64)) { return SQLite3.ColumnInt64 (stmt, index); - } else if (clrType == typeof(UInt32)) { + } + else if (clrType == typeof (UInt32)) { return (uint)SQLite3.ColumnInt64 (stmt, index); - } else if (clrType == typeof(decimal)) { + } + else if (clrType == typeof (decimal)) { return (decimal)SQLite3.ColumnDouble (stmt, index); - } else if (clrType == typeof(Byte)) { + } + else if (clrType == typeof (Byte)) { return (byte)SQLite3.ColumnInt (stmt, index); - } else if (clrType == typeof(UInt16)) { + } + else if (clrType == typeof (UInt16)) { return (ushort)SQLite3.ColumnInt (stmt, index); - } else if (clrType == typeof(Int16)) { + } + else if (clrType == typeof (Int16)) { return (short)SQLite3.ColumnInt (stmt, index); - } else if (clrType == typeof(sbyte)) { + } + else if (clrType == typeof (sbyte)) { return (sbyte)SQLite3.ColumnInt (stmt, index); - } else if (clrType == typeof(byte[])) { + } + else if (clrType == typeof (byte[])) { return SQLite3.ColumnByteArray (stmt, index); - } else if (clrType == typeof(Guid)) { - var text = SQLite3.ColumnString(stmt, index); - return new Guid(text); - } else{ + } + else if (clrType == typeof (Guid)) { + var text = SQLite3.ColumnString (stmt, index); + return new Guid (text); + } + else if (clrType == typeof(Uri)) { + var text = SQLite3.ColumnString(stmt, index); + return new Uri(text); + } + else if (clrType == typeof (StringBuilder)) { + var text = SQLite3.ColumnString (stmt, index); + return new StringBuilder (text); + } + else if (clrType == typeof(UriBuilder)) { + var text = SQLite3.ColumnString(stmt, index); + return new UriBuilder(text); + } + else { throw new NotSupportedException ("Don't know how to read " + clrType); } } @@ -2527,39 +2945,44 @@ object ReadCol (Sqlite3Statement stmt, int index, SQLite3.ColType type, Type clr /// /// Since the insert never changed, we only need to prepare once. /// - public class PreparedSqlLiteInsertCommand : IDisposable + class PreparedSqlLiteInsertCommand : IDisposable { - public bool Initialized { get; set; } + bool Initialized; - protected SQLiteConnection Connection { get; set; } + SQLiteConnection Connection; - public string CommandText { get; set; } + string CommandText; - protected Sqlite3Statement Statement { get; set; } - internal static readonly Sqlite3Statement NullStatement = default(Sqlite3Statement); + Sqlite3Statement Statement; + static readonly Sqlite3Statement NullStatement = default (Sqlite3Statement); - internal PreparedSqlLiteInsertCommand (SQLiteConnection conn) + public PreparedSqlLiteInsertCommand (SQLiteConnection conn, string commandText) { Connection = conn; + CommandText = commandText; } public int ExecuteNonQuery (object[] source) { + if (Initialized && Statement == NullStatement) { + throw new ObjectDisposedException (nameof (PreparedSqlLiteInsertCommand)); + } + if (Connection.Trace) { - Debug.WriteLine ("Executing: " + CommandText); + Connection.Tracer?.Invoke ("Executing: " + CommandText); } var r = SQLite3.Result.OK; if (!Initialized) { - Statement = Prepare (); + Statement = SQLite3.Prepare2 (Connection.Handle, CommandText); Initialized = true; } //bind the values. if (source != null) { for (int i = 0; i < source.Length; i++) { - SQLiteCommand.BindParameter (Statement, i + 1, source [i], Connection.StoreDateTimeAsTicks); + SQLiteCommand.BindParameter (Statement, i + 1, source[i], Connection.StoreDateTimeAsTicks); } } r = SQLite3.Step (Statement); @@ -2568,40 +2991,35 @@ public int ExecuteNonQuery (object[] source) int rowsAffected = SQLite3.Changes (Connection.Handle); SQLite3.Reset (Statement); return rowsAffected; - } else if (r == SQLite3.Result.Error) { + } + else if (r == SQLite3.Result.Error) { string msg = SQLite3.GetErrmsg (Connection.Handle); SQLite3.Reset (Statement); throw SQLiteException.New (r, msg); - } else if (r == SQLite3.Result.Constraint && SQLite3.ExtendedErrCode (Connection.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { + } + else if (r == SQLite3.Result.Constraint && SQLite3.ExtendedErrCode (Connection.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { SQLite3.Reset (Statement); throw NotNullConstraintViolationException.New (r, SQLite3.GetErrmsg (Connection.Handle)); - } else { + } + else { SQLite3.Reset (Statement); throw SQLiteException.New (r, r.ToString ()); } } - protected virtual Sqlite3Statement Prepare () - { - var stmt = SQLite3.Prepare2 (Connection.Handle, CommandText); - return stmt; - } - public void Dispose () { Dispose (true); GC.SuppressFinalize (this); } - private void Dispose (bool disposing) + void Dispose (bool disposing) { - if (Statement != NullStatement) { - try { - SQLite3.Finalize (Statement); - } finally { - Statement = NullStatement; - Connection = null; - } + var s = Statement; + Statement = NullStatement; + Connection = null; + if (s != NullStatement) { + SQLite3.Finalize (s); } } @@ -2611,6 +3029,22 @@ private void Dispose (bool disposing) } } + public enum CreateTableResult + { + Created, + Migrated, + } + + public class CreateTablesResult + { + public Dictionary Results { get; private set; } + + public CreateTablesResult () + { + Results = new Dictionary (); + } + } + public abstract class BaseTableQuery { protected class Ordering @@ -2636,7 +3070,7 @@ public class TableQuery : BaseTableQuery, IEnumerable BaseTableQuery _joinOuter; Expression _joinOuterKeySelector; Expression _joinSelector; - + Expression _selector; TableQuery (SQLiteConnection conn, TableMapping table) @@ -2648,7 +3082,7 @@ public class TableQuery : BaseTableQuery, IEnumerable public TableQuery (SQLiteConnection conn) { Connection = conn; - Table = Connection.GetMapping (typeof(T)); + Table = Connection.GetMapping (typeof (T)); } public TableQuery Clone () @@ -2670,6 +3104,9 @@ public TableQuery Clone () return q; } + /// + /// Filters the query based on a predicate. + /// public TableQuery Where (Expression> predExpr) { if (predExpr.NodeType == ExpressionType.Lambda) { @@ -2678,29 +3115,52 @@ public TableQuery Where (Expression> predExpr) var q = Clone (); q.AddWhere (pred); return q; - } else { + } + else { throw new NotSupportedException ("Must be a predicate"); } } - public int Delete(Expression> predExpr) + /// + /// Delete all the rows that match this query. + /// + public int Delete () { - if (predExpr.NodeType == ExpressionType.Lambda) { + return Delete (null); + } + + /// + /// Delete all the rows that match this query and the given predicate. + /// + public int Delete (Expression> predExpr) + { + if (_limit.HasValue || _offset.HasValue) + throw new InvalidOperationException ("Cannot delete with limits or offsets"); + + if (_where == null && predExpr == null) + throw new InvalidOperationException ("No condition specified"); + + var pred = _where; + + if (predExpr != null && predExpr.NodeType == ExpressionType.Lambda) { var lambda = (LambdaExpression)predExpr; - var pred = lambda.Body; - var args = new List (); - var w = CompileExpr (pred, args); - var cmdText = "delete from \"" + Table.TableName + "\""; - cmdText += " where " + w.CommandText; - var command = Connection.CreateCommand (cmdText, args.ToArray ()); - - int result = command.ExecuteNonQuery(); - return result; - } else { - throw new NotSupportedException ("Must be a predicate"); + pred = pred != null ? Expression.AndAlso (pred, lambda.Body) : lambda.Body; } + + var args = new List (); + var cmdText = "delete from \"" + Table.TableName + "\""; + var w = CompileExpr (pred, args); + cmdText += " where " + w.CommandText; + + var command = Connection.CreateCommand (cmdText, args.ToArray ()); + + int result = command.ExecuteNonQuery (); + return result; } + /// + /// Yields a given number of elements from the query and then skips the remainder. + /// public TableQuery Take (int n) { var q = Clone (); @@ -2708,6 +3168,9 @@ public TableQuery Take (int n) return q; } + /// + /// Skips a given number of elements from the query and then yields the remainder. + /// public TableQuery Skip (int n) { var q = Clone (); @@ -2715,6 +3178,9 @@ public TableQuery Skip (int n) return q; } + /// + /// Returns the element at a given index + /// public T ElementAt (int index) { return Skip (index).Take (1).First (); @@ -2728,33 +3194,45 @@ public TableQuery Deferred () return q; } + /// + /// Order the query results according to a key. + /// public TableQuery OrderBy (Expression> orderExpr) { return AddOrderBy (orderExpr, true); } + /// + /// Order the query results according to a key. + /// public TableQuery OrderByDescending (Expression> orderExpr) { return AddOrderBy (orderExpr, false); } - public TableQuery ThenBy(Expression> orderExpr) + /// + /// Order the query results according to a key. + /// + public TableQuery ThenBy (Expression> orderExpr) { - return AddOrderBy(orderExpr, true); + return AddOrderBy (orderExpr, true); } - public TableQuery ThenByDescending(Expression> orderExpr) + /// + /// Order the query results according to a key. + /// + public TableQuery ThenByDescending (Expression> orderExpr) { - return AddOrderBy(orderExpr, false); + return AddOrderBy (orderExpr, false); } - private TableQuery AddOrderBy (Expression> orderExpr, bool asc) + TableQuery AddOrderBy (Expression> orderExpr, bool asc) { if (orderExpr.NodeType == ExpressionType.Lambda) { var lambda = (LambdaExpression)orderExpr; - + MemberExpression mem = null; - + var unary = lambda.Body as UnaryExpression; if (unary != null && unary.NodeType == ExpressionType.Convert) { mem = unary.Operand as MemberExpression; @@ -2762,21 +3240,23 @@ private TableQuery AddOrderBy (Expression> orderExpr, bool asc) else { mem = lambda.Body as MemberExpression; } - + if (mem != null && (mem.Expression.NodeType == ExpressionType.Parameter)) { var q = Clone (); if (q._orderBys == null) { q._orderBys = new List (); } q._orderBys.Add (new Ordering { - ColumnName = Table.FindColumnWithPropertyName(mem.Member.Name).Name, + ColumnName = Table.FindColumnWithPropertyName (mem.Member.Name).Name, Ascending = asc }); return q; - } else { + } + else { throw new NotSupportedException ("Order By does not support: " + orderExpr); } - } else { + } + else { throw new NotSupportedException ("Must be a predicate"); } } @@ -2785,33 +3265,39 @@ private void AddWhere (Expression pred) { if (_where == null) { _where = pred; - } else { + } + else { _where = Expression.AndAlso (_where, pred); } } - - public TableQuery Join ( - TableQuery inner, - Expression> outerKeySelector, - Expression> innerKeySelector, - Expression> resultSelector) - { - var q = new TableQuery (Connection, Connection.GetMapping (typeof (TResult))) { - _joinOuter = this, - _joinOuterKeySelector = outerKeySelector, - _joinInner = inner, - _joinInnerKeySelector = innerKeySelector, - _joinSelector = resultSelector, - }; - return q; - } - - public TableQuery Select (Expression> selector) - { - var q = Clone (); - q._selector = selector; - return q; - } + + ///// + ///// Performs an inner join of two queries based on matching keys extracted from the elements. + ///// + //public TableQuery Join ( + // TableQuery inner, + // Expression> outerKeySelector, + // Expression> innerKeySelector, + // Expression> resultSelector) + //{ + // var q = new TableQuery (Connection, Connection.GetMapping (typeof (TResult))) { + // _joinOuter = this, + // _joinOuterKeySelector = outerKeySelector, + // _joinInner = inner, + // _joinInnerKeySelector = innerKeySelector, + // _joinSelector = resultSelector, + // }; + // return q; + //} + + // Not needed until Joins are supported + // Keeping this commented out forces the default Linq to objects processor to run + //public TableQuery Select (Expression> selector) + //{ + // var q = Clone (); + // q._selector = selector; + // return q; + //} private SQLiteCommand GenerateCommand (string selectionList) { @@ -2853,16 +3339,17 @@ private CompileResult CompileExpr (Expression expr, List queryArgs) { if (expr == null) { throw new NotSupportedException ("Expression is NULL"); - } else if (expr is BinaryExpression) { + } + else if (expr is BinaryExpression) { var bin = (BinaryExpression)expr; // VB turns 'x=="foo"' into 'CompareString(x,"foo",true/false)==0', so we need to unwrap it // http://blogs.msdn.com/b/vbteam/archive/2007/09/18/vb-expression-trees-string-comparisons.aspx - if (bin.Left.NodeType == ExpressionType.Call) { + if (bin.Left.NodeType == ExpressionType.Call) { var call = (MethodCallExpression)bin.Left; if (call.Method.DeclaringType.FullName == "Microsoft.VisualBasic.CompilerServices.Operators" && call.Method.Name == "CompareString") - bin = Expression.MakeBinary(bin.NodeType, call.Arguments[0], call.Arguments[1]); + bin = Expression.MakeBinary (bin.NodeType, call.Arguments[0], call.Arguments[1]); } @@ -2872,74 +3359,110 @@ private CompileResult CompileExpr (Expression expr, List queryArgs) //If either side is a parameter and is null, then handle the other side specially (for "is null"/"is not null") string text; if (leftr.CommandText == "?" && leftr.Value == null) - text = CompileNullBinaryExpression(bin, rightr); + text = CompileNullBinaryExpression (bin, rightr); else if (rightr.CommandText == "?" && rightr.Value == null) - text = CompileNullBinaryExpression(bin, leftr); + text = CompileNullBinaryExpression (bin, leftr); else - text = "(" + leftr.CommandText + " " + GetSqlName(bin) + " " + rightr.CommandText + ")"; + text = "(" + leftr.CommandText + " " + GetSqlName (bin) + " " + rightr.CommandText + ")"; return new CompileResult { CommandText = text }; - } else if (expr.NodeType == ExpressionType.Not) { - var operandExpr = ((UnaryExpression)expr).Operand; - var opr = CompileExpr(operandExpr, queryArgs); - object val = opr.Value; - if (val is bool) - val = !((bool) val); - return new CompileResult - { - CommandText = "NOT(" + opr.CommandText + ")", - Value = val - }; - } else if (expr.NodeType == ExpressionType.Call) { - + } + else if (expr.NodeType == ExpressionType.Not) { + var operandExpr = ((UnaryExpression)expr).Operand; + var opr = CompileExpr (operandExpr, queryArgs); + object val = opr.Value; + if (val is bool) + val = !((bool)val); + return new CompileResult { + CommandText = "NOT(" + opr.CommandText + ")", + Value = val + }; + } + else if (expr.NodeType == ExpressionType.Call) { + var call = (MethodCallExpression)expr; var args = new CompileResult[call.Arguments.Count]; var obj = call.Object != null ? CompileExpr (call.Object, queryArgs) : null; - + for (var i = 0; i < args.Length; i++) { - args [i] = CompileExpr (call.Arguments [i], queryArgs); + args[i] = CompileExpr (call.Arguments[i], queryArgs); } - + var sqlCall = ""; - + if (call.Method.Name == "Like" && args.Length == 2) { - sqlCall = "(" + args [0].CommandText + " like " + args [1].CommandText + ")"; + sqlCall = "(" + args[0].CommandText + " like " + args[1].CommandText + ")"; } else if (call.Method.Name == "Contains" && args.Length == 2) { - sqlCall = "(" + args [1].CommandText + " in " + args [0].CommandText + ")"; + sqlCall = "(" + args[1].CommandText + " in " + args[0].CommandText + ")"; } else if (call.Method.Name == "Contains" && args.Length == 1) { - if (call.Object != null && call.Object.Type == typeof(string)) { - sqlCall = "(" + obj.CommandText + " like ('%' || " + args [0].CommandText + " || '%'))"; + if (call.Object != null && call.Object.Type == typeof (string)) { + sqlCall = "( instr(" + obj.CommandText + "," + args[0].CommandText + ") >0 )"; } else { - sqlCall = "(" + args [0].CommandText + " in " + obj.CommandText + ")"; + sqlCall = "(" + args[0].CommandText + " in " + obj.CommandText + ")"; } } - else if (call.Method.Name == "StartsWith" && args.Length == 1) { - sqlCall = "(" + obj.CommandText + " like (" + args [0].CommandText + " || '%'))"; + else if (call.Method.Name == "StartsWith" && args.Length >= 1) { + var startsWithCmpOp = StringComparison.CurrentCulture; + if (args.Length == 2) { + startsWithCmpOp = (StringComparison)args[1].Value; + } + switch (startsWithCmpOp) { + case StringComparison.Ordinal: + case StringComparison.CurrentCulture: + sqlCall = "( substr(" + obj.CommandText + ", 1, " + args[0].Value.ToString ().Length + ") = " + args[0].CommandText + ")"; + break; + case StringComparison.OrdinalIgnoreCase: + case StringComparison.CurrentCultureIgnoreCase: + sqlCall = "(" + obj.CommandText + " like (" + args[0].CommandText + " || '%'))"; + break; + } + } - else if (call.Method.Name == "EndsWith" && args.Length == 1) { - sqlCall = "(" + obj.CommandText + " like ('%' || " + args [0].CommandText + "))"; + else if (call.Method.Name == "EndsWith" && args.Length >= 1) { + var endsWithCmpOp = StringComparison.CurrentCulture; + if (args.Length == 2) { + endsWithCmpOp = (StringComparison)args[1].Value; + } + switch (endsWithCmpOp) { + case StringComparison.Ordinal: + case StringComparison.CurrentCulture: + sqlCall = "( substr(" + obj.CommandText + ", length(" + obj.CommandText + ") - " + args[0].Value.ToString ().Length + "+1, " + args[0].Value.ToString ().Length + ") = " + args[0].CommandText + ")"; + break; + case StringComparison.OrdinalIgnoreCase: + case StringComparison.CurrentCultureIgnoreCase: + sqlCall = "(" + obj.CommandText + " like ('%' || " + args[0].CommandText + "))"; + break; + } } else if (call.Method.Name == "Equals" && args.Length == 1) { sqlCall = "(" + obj.CommandText + " = (" + args[0].CommandText + "))"; - } else if (call.Method.Name == "ToLower") { - sqlCall = "(lower(" + obj.CommandText + "))"; - } else if (call.Method.Name == "ToUpper") { - sqlCall = "(upper(" + obj.CommandText + "))"; - } else { + } + else if (call.Method.Name == "ToLower") { + sqlCall = "(lower(" + obj.CommandText + "))"; + } + else if (call.Method.Name == "ToUpper") { + sqlCall = "(upper(" + obj.CommandText + "))"; + } + else if (call.Method.Name == "Replace" && args.Length == 2) { + sqlCall = "(replace(" + obj.CommandText + "," + args[0].CommandText + "," + args[1].CommandText + "))"; + } + else { sqlCall = call.Method.Name.ToLower () + "(" + string.Join (",", args.Select (a => a.CommandText).ToArray ()) + ")"; } return new CompileResult { CommandText = sqlCall }; - - } else if (expr.NodeType == ExpressionType.Constant) { + + } + else if (expr.NodeType == ExpressionType.Constant) { var c = (ConstantExpression)expr; queryArgs.Add (c.Value); return new CompileResult { CommandText = "?", Value = c.Value }; - } else if (expr.NodeType == ExpressionType.Convert) { + } + else if (expr.NodeType == ExpressionType.Convert) { var u = (UnaryExpression)expr; var ty = u.Type; var valr = CompileExpr (u.Operand, queryArgs); @@ -2947,17 +3470,27 @@ private CompileResult CompileExpr (Expression expr, List queryArgs) CommandText = valr.CommandText, Value = valr.Value != null ? ConvertTo (valr.Value, ty) : null }; - } else if (expr.NodeType == ExpressionType.MemberAccess) { + } + else if (expr.NodeType == ExpressionType.MemberAccess) { var mem = (MemberExpression)expr; - - if (mem.Expression!=null && mem.Expression.NodeType == ExpressionType.Parameter) { + + var paramExpr = mem.Expression as ParameterExpression; + if (paramExpr == null) { + var convert = mem.Expression as UnaryExpression; + if (convert != null && convert.NodeType == ExpressionType.Convert) { + paramExpr = convert.Operand as ParameterExpression; + } + } + + if (paramExpr != null) { // // This is a column of our table, output just the column name // Need to translate it if that column name is mapped // var columnName = Table.FindColumnWithPropertyName (mem.Member.Name).Name; return new CompileResult { CommandText = "\"" + columnName + "\"" }; - } else { + } + else { object obj = null; if (mem.Expression != null) { var r = CompileExpr (mem.Expression, queryArgs); @@ -2969,54 +3502,40 @@ private CompileResult CompileExpr (Expression expr, List queryArgs) } obj = r.Value; } - + // // Get the member value // object val = null; - -#if !USE_NEW_REFLECTION_API - if (mem.Member.MemberType == MemberTypes.Property) { -#else + if (mem.Member is PropertyInfo) { -#endif var m = (PropertyInfo)mem.Member; val = m.GetValue (obj, null); -#if !USE_NEW_REFLECTION_API - } else if (mem.Member.MemberType == MemberTypes.Field) { -#else - } else if (mem.Member is FieldInfo) { -#endif -#if SILVERLIGHT - val = Expression.Lambda (expr).Compile ().DynamicInvoke (); -#else + } + else if (mem.Member is FieldInfo) { var m = (FieldInfo)mem.Member; val = m.GetValue (obj); -#endif - } else { -#if !USE_NEW_REFLECTION_API - throw new NotSupportedException ("MemberExpr: " + mem.Member.MemberType); -#else - throw new NotSupportedException ("MemberExpr: " + mem.Member.DeclaringType); -#endif } - + else { + throw new NotSupportedException ("MemberExpr: " + mem.Member.GetType ()); + } + // // Work special magic for enumerables // if (val != null && val is System.Collections.IEnumerable && !(val is string) && !(val is System.Collections.Generic.IEnumerable)) { - var sb = new System.Text.StringBuilder(); - sb.Append("("); + var sb = new System.Text.StringBuilder (); + sb.Append ("("); var head = ""; foreach (var a in (System.Collections.IEnumerable)val) { - queryArgs.Add(a); - sb.Append(head); - sb.Append("?"); + queryArgs.Add (a); + sb.Append (head); + sb.Append ("?"); head = ","; } - sb.Append(")"); + sb.Append (")"); return new CompileResult { - CommandText = sb.ToString(), + CommandText = sb.ToString (), Value = val }; } @@ -3034,12 +3553,13 @@ private CompileResult CompileExpr (Expression expr, List queryArgs) static object ConvertTo (object obj, Type t) { - Type nut = Nullable.GetUnderlyingType(t); - + Type nut = Nullable.GetUnderlyingType (t); + if (nut != null) { - if (obj == null) return null; + if (obj == null) return null; return Convert.ChangeType (obj, nut); - } else { + } + else { return Convert.ChangeType (obj, t); } } @@ -3047,49 +3567,71 @@ static object ConvertTo (object obj, Type t) /// /// Compiles a BinaryExpression where one of the parameters is null. /// + /// The expression to compile /// The non-null parameter - private string CompileNullBinaryExpression(BinaryExpression expression, CompileResult parameter) + private string CompileNullBinaryExpression (BinaryExpression expression, CompileResult parameter) { if (expression.NodeType == ExpressionType.Equal) return "(" + parameter.CommandText + " is ?)"; else if (expression.NodeType == ExpressionType.NotEqual) - return "(" + parameter.CommandText + " is not ?)"; - else - throw new NotSupportedException("Cannot compile Null-BinaryExpression with type " + expression.NodeType.ToString()); + return "(" + parameter.CommandText + " is not ?)"; + else if (expression.NodeType == ExpressionType.GreaterThan + || expression.NodeType == ExpressionType.GreaterThanOrEqual + || expression.NodeType == ExpressionType.LessThan + || expression.NodeType == ExpressionType.LessThanOrEqual) + return "(" + parameter.CommandText + " < ?)"; // always false + else + throw new NotSupportedException ("Cannot compile Null-BinaryExpression with type " + expression.NodeType.ToString ()); } string GetSqlName (Expression expr) { var n = expr.NodeType; if (n == ExpressionType.GreaterThan) - return ">"; else if (n == ExpressionType.GreaterThanOrEqual) { + return ">"; + else if (n == ExpressionType.GreaterThanOrEqual) { return ">="; - } else if (n == ExpressionType.LessThan) { + } + else if (n == ExpressionType.LessThan) { return "<"; - } else if (n == ExpressionType.LessThanOrEqual) { + } + else if (n == ExpressionType.LessThanOrEqual) { return "<="; - } else if (n == ExpressionType.And) { + } + else if (n == ExpressionType.And) { return "&"; - } else if (n == ExpressionType.AndAlso) { + } + else if (n == ExpressionType.AndAlso) { return "and"; - } else if (n == ExpressionType.Or) { + } + else if (n == ExpressionType.Or) { return "|"; - } else if (n == ExpressionType.OrElse) { + } + else if (n == ExpressionType.OrElse) { return "or"; - } else if (n == ExpressionType.Equal) { + } + else if (n == ExpressionType.Equal) { return "="; - } else if (n == ExpressionType.NotEqual) { + } + else if (n == ExpressionType.NotEqual) { return "!="; - } else { + } + else { throw new NotSupportedException ("Cannot get SQL for: " + n); } } - + + /// + /// Execute SELECT COUNT(*) on the query + /// public int Count () { - return GenerateCommand("count(*)").ExecuteScalar (); + return GenerateCommand ("count(*)").ExecuteScalar (); } + /// + /// Execute SELECT COUNT(*) on the query with an additional WHERE clause. + /// public int Count (Expression> predExpr) { return Where (predExpr).Count (); @@ -3098,9 +3640,9 @@ public int Count (Expression> predExpr) public IEnumerator GetEnumerator () { if (!_deferred) - return GenerateCommand("*").ExecuteQuery().GetEnumerator(); + return GenerateCommand ("*").ExecuteQuery ().GetEnumerator (); - return GenerateCommand("*").ExecuteDeferredQuery().GetEnumerator(); + return GenerateCommand ("*").ExecuteDeferredQuery ().GetEnumerator (); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator () @@ -3108,28 +3650,57 @@ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator () return GetEnumerator (); } + /// + /// Queries the database and returns the results as a List. + /// + public List ToList () + { + return GenerateCommand ("*").ExecuteQuery (); + } + + /// + /// Queries the database and returns the results as an array. + /// + public T[] ToArray () + { + return GenerateCommand ("*").ExecuteQuery ().ToArray (); + } + + /// + /// Returns the first element of this query. + /// public T First () { var query = Take (1); - return query.ToList().First (); + return query.ToList ().First (); } + /// + /// Returns the first element of this query, or null if no element is found. + /// public T FirstOrDefault () { var query = Take (1); - return query.ToList().FirstOrDefault (); + return query.ToList ().FirstOrDefault (); } + /// + /// Returns the first element of this query that matches the predicate. + /// public T First (Expression> predExpr) { return Where (predExpr).First (); } + /// + /// Returns the first element of this query that matches the predicate, or null + /// if no element is found. + /// public T FirstOrDefault (Expression> predExpr) { return Where (predExpr).FirstOrDefault (); } - } + } public static class SQLite3 { @@ -3216,7 +3787,7 @@ public enum ExtendedResult : int NoticeRecoverWAL = (Result.Notice | (1 << 8)), NoticeRecoverRollback = (Result.Notice | (2 << 8)) } - + public enum ConfigOption : int { @@ -3236,7 +3807,7 @@ public enum ConfigOption : int [DllImport(LibraryPath, EntryPoint = "sqlite3_open_v2", CallingConvention=CallingConvention.Cdecl)] public static extern Result Open ([MarshalAs(UnmanagedType.LPStr)] string filename, out IntPtr db, int flags, IntPtr zvfs); - + [DllImport(LibraryPath, EntryPoint = "sqlite3_open_v2", CallingConvention = CallingConvention.Cdecl)] public static extern Result Open(byte[] filename, out IntPtr db, int flags, IntPtr zvfs); @@ -3248,16 +3819,16 @@ public enum ConfigOption : int [DllImport(LibraryPath, EntryPoint = "sqlite3_close", CallingConvention=CallingConvention.Cdecl)] public static extern Result Close (IntPtr db); - + [DllImport(LibraryPath, EntryPoint = "sqlite3_close_v2", CallingConvention = CallingConvention.Cdecl)] public static extern Result Close2(IntPtr db); - + [DllImport(LibraryPath, EntryPoint = "sqlite3_initialize", CallingConvention=CallingConvention.Cdecl)] public static extern Result Initialize(); - + [DllImport(LibraryPath, EntryPoint = "sqlite3_shutdown", CallingConvention=CallingConvention.Cdecl)] public static extern Result Shutdown(); - + [DllImport(LibraryPath, EntryPoint = "sqlite3_config", CallingConvention=CallingConvention.Cdecl)] public static extern Result Config (ConfigOption option); @@ -3391,203 +3962,209 @@ public static byte[] ColumnByteArray (IntPtr stmt, int index) [DllImport (LibraryPath, EntryPoint = "sqlite3_libversion_number", CallingConvention = CallingConvention.Cdecl)] public static extern int LibVersionNumber (); #else - public static Result Open(string filename, out Sqlite3DatabaseHandle db) + public static Result Open (string filename, out Sqlite3DatabaseHandle db) { - return (Result) Sqlite3.sqlite3_open(filename, out db); + return (Result)Sqlite3.sqlite3_open (filename, out db); } - public static Result Open(string filename, out Sqlite3DatabaseHandle db, int flags, IntPtr zVfs) + public static Result Open (string filename, out Sqlite3DatabaseHandle db, int flags, IntPtr zVfs) { #if USE_WP8_NATIVE_SQLITE return (Result)Sqlite3.sqlite3_open_v2(filename, out db, flags, ""); #else - return (Result)Sqlite3.sqlite3_open_v2(filename, out db, flags, null); + return (Result)Sqlite3.sqlite3_open_v2 (filename, out db, flags, null); #endif } - public static Result Close(Sqlite3DatabaseHandle db) + public static Result Close (Sqlite3DatabaseHandle db) { - return (Result)Sqlite3.sqlite3_close(db); + return (Result)Sqlite3.sqlite3_close (db); } - public static Result Close2(Sqlite3DatabaseHandle db) + public static Result Close2 (Sqlite3DatabaseHandle db) { - return (Result)Sqlite3.sqlite3_close_v2(db); + return (Result)Sqlite3.sqlite3_close_v2 (db); } - public static Result BusyTimeout(Sqlite3DatabaseHandle db, int milliseconds) + public static Result BusyTimeout (Sqlite3DatabaseHandle db, int milliseconds) { - return (Result)Sqlite3.sqlite3_busy_timeout(db, milliseconds); + return (Result)Sqlite3.sqlite3_busy_timeout (db, milliseconds); } - public static int Changes(Sqlite3DatabaseHandle db) + public static int Changes (Sqlite3DatabaseHandle db) { - return Sqlite3.sqlite3_changes(db); + return Sqlite3.sqlite3_changes (db); } - public static Sqlite3Statement Prepare2(Sqlite3DatabaseHandle db, string query) + public static Sqlite3Statement Prepare2 (Sqlite3DatabaseHandle db, string query) { - Sqlite3Statement stmt = default(Sqlite3Statement); + Sqlite3Statement stmt = default (Sqlite3Statement); #if USE_WP8_NATIVE_SQLITE || USE_SQLITEPCL_RAW - var r = Sqlite3.sqlite3_prepare_v2(db, query, out stmt); + var r = Sqlite3.sqlite3_prepare_v2 (db, query, out stmt); #else stmt = new Sqlite3Statement(); var r = Sqlite3.sqlite3_prepare_v2(db, query, -1, ref stmt, 0); #endif - if (r != 0) - { - throw SQLiteException.New((Result)r, GetErrmsg(db)); + if (r != 0) { + throw SQLiteException.New ((Result)r, GetErrmsg (db)); } return stmt; } - public static Result Step(Sqlite3Statement stmt) + public static Result Step (Sqlite3Statement stmt) { - return (Result)Sqlite3.sqlite3_step(stmt); + return (Result)Sqlite3.sqlite3_step (stmt); } - public static Result Reset(Sqlite3Statement stmt) + public static Result Reset (Sqlite3Statement stmt) { - return (Result)Sqlite3.sqlite3_reset(stmt); + return (Result)Sqlite3.sqlite3_reset (stmt); } - public static Result Finalize(Sqlite3Statement stmt) + public static Result Finalize (Sqlite3Statement stmt) { - return (Result)Sqlite3.sqlite3_finalize(stmt); + return (Result)Sqlite3.sqlite3_finalize (stmt); } - public static long LastInsertRowid(Sqlite3DatabaseHandle db) + public static long LastInsertRowid (Sqlite3DatabaseHandle db) { - return Sqlite3.sqlite3_last_insert_rowid(db); + return Sqlite3.sqlite3_last_insert_rowid (db); } - public static string GetErrmsg(Sqlite3DatabaseHandle db) + public static string GetErrmsg (Sqlite3DatabaseHandle db) { - return Sqlite3.sqlite3_errmsg(db); + return Sqlite3.sqlite3_errmsg (db); } - public static int BindParameterIndex(Sqlite3Statement stmt, string name) + public static int BindParameterIndex (Sqlite3Statement stmt, string name) { - return Sqlite3.sqlite3_bind_parameter_index(stmt, name); + return Sqlite3.sqlite3_bind_parameter_index (stmt, name); } - public static int BindNull(Sqlite3Statement stmt, int index) + public static int BindNull (Sqlite3Statement stmt, int index) { - return Sqlite3.sqlite3_bind_null(stmt, index); + return Sqlite3.sqlite3_bind_null (stmt, index); } - public static int BindInt(Sqlite3Statement stmt, int index, int val) + public static int BindInt (Sqlite3Statement stmt, int index, int val) { - return Sqlite3.sqlite3_bind_int(stmt, index, val); + return Sqlite3.sqlite3_bind_int (stmt, index, val); } - public static int BindInt64(Sqlite3Statement stmt, int index, long val) + public static int BindInt64 (Sqlite3Statement stmt, int index, long val) { - return Sqlite3.sqlite3_bind_int64(stmt, index, val); + return Sqlite3.sqlite3_bind_int64 (stmt, index, val); } - public static int BindDouble(Sqlite3Statement stmt, int index, double val) + public static int BindDouble (Sqlite3Statement stmt, int index, double val) { - return Sqlite3.sqlite3_bind_double(stmt, index, val); + return Sqlite3.sqlite3_bind_double (stmt, index, val); } - public static int BindText(Sqlite3Statement stmt, int index, string val, int n, IntPtr free) + public static int BindText (Sqlite3Statement stmt, int index, string val, int n, IntPtr free) { #if USE_WP8_NATIVE_SQLITE return Sqlite3.sqlite3_bind_text(stmt, index, val, n); #elif USE_SQLITEPCL_RAW - return Sqlite3.sqlite3_bind_text(stmt, index, val); + return Sqlite3.sqlite3_bind_text (stmt, index, val); #else return Sqlite3.sqlite3_bind_text(stmt, index, val, n, null); #endif } - public static int BindBlob(Sqlite3Statement stmt, int index, byte[] val, int n, IntPtr free) + public static int BindBlob (Sqlite3Statement stmt, int index, byte[] val, int n, IntPtr free) { #if USE_WP8_NATIVE_SQLITE return Sqlite3.sqlite3_bind_blob(stmt, index, val, n); #elif USE_SQLITEPCL_RAW - return Sqlite3.sqlite3_bind_blob(stmt, index, val); + return Sqlite3.sqlite3_bind_blob (stmt, index, val); #else return Sqlite3.sqlite3_bind_blob(stmt, index, val, n, null); #endif } - public static int ColumnCount(Sqlite3Statement stmt) + public static int ColumnCount (Sqlite3Statement stmt) { - return Sqlite3.sqlite3_column_count(stmt); + return Sqlite3.sqlite3_column_count (stmt); } - public static string ColumnName(Sqlite3Statement stmt, int index) + public static string ColumnName (Sqlite3Statement stmt, int index) { - return Sqlite3.sqlite3_column_name(stmt, index); + return Sqlite3.sqlite3_column_name (stmt, index); } - public static string ColumnName16(Sqlite3Statement stmt, int index) + public static string ColumnName16 (Sqlite3Statement stmt, int index) { - return Sqlite3.sqlite3_column_name(stmt, index); + return Sqlite3.sqlite3_column_name (stmt, index); } - public static ColType ColumnType(Sqlite3Statement stmt, int index) + public static ColType ColumnType (Sqlite3Statement stmt, int index) { - return (ColType)Sqlite3.sqlite3_column_type(stmt, index); + return (ColType)Sqlite3.sqlite3_column_type (stmt, index); } - public static int ColumnInt(Sqlite3Statement stmt, int index) + public static int ColumnInt (Sqlite3Statement stmt, int index) { - return Sqlite3.sqlite3_column_int(stmt, index); + return Sqlite3.sqlite3_column_int (stmt, index); } - public static long ColumnInt64(Sqlite3Statement stmt, int index) + public static long ColumnInt64 (Sqlite3Statement stmt, int index) { - return Sqlite3.sqlite3_column_int64(stmt, index); + return Sqlite3.sqlite3_column_int64 (stmt, index); } - public static double ColumnDouble(Sqlite3Statement stmt, int index) + public static double ColumnDouble (Sqlite3Statement stmt, int index) { - return Sqlite3.sqlite3_column_double(stmt, index); + return Sqlite3.sqlite3_column_double (stmt, index); } - public static string ColumnText(Sqlite3Statement stmt, int index) + public static string ColumnText (Sqlite3Statement stmt, int index) { - return Sqlite3.sqlite3_column_text(stmt, index); + return Sqlite3.sqlite3_column_text (stmt, index); } - public static string ColumnText16(Sqlite3Statement stmt, int index) + public static string ColumnText16 (Sqlite3Statement stmt, int index) { - return Sqlite3.sqlite3_column_text(stmt, index); + return Sqlite3.sqlite3_column_text (stmt, index); } - public static byte[] ColumnBlob(Sqlite3Statement stmt, int index) + public static byte[] ColumnBlob (Sqlite3Statement stmt, int index) { - return Sqlite3.sqlite3_column_blob(stmt, index); + return Sqlite3.sqlite3_column_blob (stmt, index); } - public static int ColumnBytes(Sqlite3Statement stmt, int index) + public static int ColumnBytes (Sqlite3Statement stmt, int index) { - return Sqlite3.sqlite3_column_bytes(stmt, index); + return Sqlite3.sqlite3_column_bytes (stmt, index); } - public static string ColumnString(Sqlite3Statement stmt, int index) + public static string ColumnString (Sqlite3Statement stmt, int index) { - return Sqlite3.sqlite3_column_text(stmt, index); + return Sqlite3.sqlite3_column_text (stmt, index); + } + + public static byte[] ColumnByteArray (Sqlite3Statement stmt, int index) + { + int length = ColumnBytes (stmt, index); + if (length > 0) { + return ColumnBlob (stmt, index); + } + return new byte[0]; } - public static byte[] ColumnByteArray(Sqlite3Statement stmt, int index) + public static Result EnableLoadExtension (Sqlite3DatabaseHandle db, int onoff) { - return ColumnBlob(stmt, index); + return (Result)Sqlite3.sqlite3_enable_load_extension (db, onoff); } -#if !USE_SQLITEPCL_RAW - public static Result EnableLoadExtension(Sqlite3DatabaseHandle db, int onoff) + public static int LibVersionNumber () { - return (Result)Sqlite3.sqlite3_enable_load_extension(db, onoff); + return Sqlite3.sqlite3_libversion_number (); } -#endif - public static ExtendedResult ExtendedErrCode(Sqlite3DatabaseHandle db) + public static ExtendedResult ExtendedErrCode (Sqlite3DatabaseHandle db) { - return (ExtendedResult)Sqlite3.sqlite3_extended_errcode(db); + return (ExtendedResult)Sqlite3.sqlite3_extended_errcode (db); } #endif @@ -3601,24 +4178,3 @@ public enum ColType : int } } } - -#if NO_CONCURRENT -namespace SQLite.Extensions -{ - public static class ListEx - { - public static bool TryAdd (this IDictionary dict, TKey key, TValue value) - { - try { - dict.Add (key, value); - return true; - } - catch (ArgumentException) { - return false; - } - } - } -} -#endif - - diff --git a/src/SQLiteAsync.cs b/src/SQLiteAsync.cs index 25ff372fc..2823748a9 100644 --- a/src/SQLiteAsync.cs +++ b/src/SQLiteAsync.cs @@ -1,5 +1,5 @@ // -// Copyright (c) 2012-2016 Krueger Systems, Inc. +// Copyright (c) 2012-2017 Krueger Systems, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -28,265 +28,975 @@ using System.Threading; using System.Threading.Tasks; -#pragma warning disable 1591 // XML Doc Comments - namespace SQLite { + /// + /// A pooled asynchronous connection to a SQLite database. + /// public partial class SQLiteAsyncConnection { SQLiteConnectionString _connectionString; - SQLiteOpenFlags _openFlags; + SQLiteConnectionWithLock _fullMutexReadConnection; + bool isFullMutex; + SQLiteOpenFlags _openFlags; - public SQLiteAsyncConnection(string databasePath, bool storeDateTimeAsTicks = true) - : this(databasePath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create, storeDateTimeAsTicks) - { - } - - public SQLiteAsyncConnection(string databasePath, SQLiteOpenFlags openFlags, bool storeDateTimeAsTicks = true) - { - _openFlags = openFlags; - _connectionString = new SQLiteConnectionString(databasePath, storeDateTimeAsTicks); - } + /// + /// Constructs a new SQLiteAsyncConnection and opens a pooled SQLite database specified by databasePath. + /// + /// + /// Specifies the path to the database file. + /// + /// + /// Specifies whether to store DateTime properties as ticks (true) or strings (false). You + /// absolutely do want to store them as Ticks in all new projects. The value of false is + /// only here for backwards compatibility. There is a *significant* speed advantage, with no + /// down sides, when setting storeDateTimeAsTicks = true. + /// If you use DateTimeOffset properties, it will be always stored as ticks regardingless + /// the storeDateTimeAsTicks parameter. + /// + public SQLiteAsyncConnection (string databasePath, bool storeDateTimeAsTicks = true) + : this (databasePath, SQLiteOpenFlags.FullMutex | SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create, storeDateTimeAsTicks) + { + } - public static void ResetPool() + /// + /// Constructs a new SQLiteAsyncConnection and opens a pooled SQLite database specified by databasePath. + /// + /// + /// Specifies the path to the database file. + /// + /// + /// Flags controlling how the connection should be opened. + /// + /// + /// Specifies whether to store DateTime properties as ticks (true) or strings (false). You + /// absolutely do want to store them as Ticks in all new projects. The value of false is + /// only here for backwards compatibility. There is a *significant* speed advantage, with no + /// down sides, when setting storeDateTimeAsTicks = true. + /// If you use DateTimeOffset properties, it will be always stored as ticks regardingless + /// the storeDateTimeAsTicks parameter. + /// + public SQLiteAsyncConnection (string databasePath, SQLiteOpenFlags openFlags, bool storeDateTimeAsTicks = true) { - SQLiteConnectionPool.Shared.Reset(); + _openFlags = openFlags; + isFullMutex = _openFlags.HasFlag (SQLiteOpenFlags.FullMutex); + _connectionString = new SQLiteConnectionString (databasePath, storeDateTimeAsTicks); + if(isFullMutex) + _fullMutexReadConnection = new SQLiteConnectionWithLock (_connectionString, openFlags) { SkipLock = true }; } + /// + /// Gets the database path used by this connection. + /// + public string DatabasePath => GetConnection ().DatabasePath; + + /// + /// Gets the SQLite library version number. 3007014 would be v3.7.14 + /// + public int LibVersionNumber => GetConnection ().LibVersionNumber; + + /// + /// The amount of time to wait for a table to become unlocked. + /// + public TimeSpan GetBusyTimeout () + { + return GetConnection ().BusyTimeout; + } + + /// + /// Sets the amount of time to wait for a table to become unlocked. + /// + public Task SetBusyTimeoutAsync (TimeSpan value) + { + return ReadAsync (conn => { + conn.BusyTimeout = value; + return null; + }); + } + + /// + /// Whether to store DateTime properties as ticks (true) or strings (false). + /// + public bool StoreDateTimeAsTicks => GetConnection ().StoreDateTimeAsTicks; + + /// + /// Whether to writer queries to during execution. + /// + /// The tracer. + public bool Trace { + get { return GetConnection ().Trace; } + set { GetConnection ().Trace = value; } + } + + /// + /// The delegate responsible for writing trace lines. + /// + /// The tracer. + public Action Tracer { + get { return GetConnection ().Tracer; } + set { GetConnection ().Tracer = value; } + } + + /// + /// Whether Trace lines should be written that show the execution time of queries. + /// + public bool TimeExecution { + get { return GetConnection ().TimeExecution; } + set { GetConnection ().TimeExecution = value; } + } + + /// + /// Returns the mappings from types to tables that the connection + /// currently understands. + /// + public IEnumerable TableMappings => GetConnection ().TableMappings; + + /// + /// Closes all connections to all async databases. + /// You should *never* need to do this. + /// This is a blocking operation that will return when all connections + /// have been closed. + /// + public static void ResetPool () + { + SQLiteConnectionPool.Shared.Reset (); + } + + /// + /// Gets the pooled lockable connection used by this async connection. + /// You should never need to use this. This is provided only to add additional + /// functionality to SQLite-net. If you use this connection, you must use + /// the Lock method on it while using it. + /// public SQLiteConnectionWithLock GetConnection () { return SQLiteConnectionPool.Shared.GetConnection (_connectionString, _openFlags); } - public Task CreateTableAsync (CreateFlags createFlags = CreateFlags.None) - where T : new () + /// + /// Closes any pooled connections used by the database. + /// + public Task CloseAsync () + { + return Task.Factory.StartNew (() => { + SQLiteConnectionPool.Shared.CloseConnection (_connectionString, _openFlags); + }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + } + + Task ReadAsync (Func read) + { + return Task.Factory.StartNew (() => { + var conn = isFullMutex ? _fullMutexReadConnection : GetConnection (); + using (conn.Lock ()) { + return read (conn); + } + }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + } + + Task WriteAsync (Func write) { - return CreateTablesAsync (createFlags, typeof(T)); + return Task.Factory.StartNew (() => { + var conn = GetConnection (); + using (conn.Lock ()) { + return write (conn); + } + }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } + /// + /// Sets the key used to encrypt/decrypt the database. + /// This must be the first thing you call before doing anything else with this connection + /// if your database is encrypted. + /// This only has an effect if you are using the SQLCipher nuget package. + /// + /// Ecryption key plain text that is converted to the real encryption key using PBKDF2 key derivation + public Task SetKeyAsync (string key) + { + if (key == null) throw new ArgumentNullException (nameof (key)); + return WriteAsync (conn => { + conn.SetKey (key); + return null; + }); + } + + /// + /// Sets the key used to encrypt/decrypt the database. + /// This must be the first thing you call before doing anything else with this connection + /// if your database is encrypted. + /// This only has an effect if you are using the SQLCipher nuget package. + /// + /// 256-bit (32 byte) ecryption key data + public Task SetKeyAsync (byte[] key) + { + if (key == null) throw new ArgumentNullException (nameof (key)); + if (key.Length != 32) throw new ArgumentException ("Key must be 32 bytes (256-bit)", nameof (key)); + return WriteAsync (conn => { + conn.SetKey (key); + return null; + }); + } + + /// + /// Enable or disable extension loading. + /// + public Task EnableLoadExtensionAsync (bool enabled) + { + return WriteAsync (conn => { + conn.EnableLoadExtension (enabled); + return null; + }); + } + + /// + /// Executes a "create table if not exists" on the database. It also + /// creates any specified indexes on the columns of the table. It uses + /// a schema automatically generated from the specified type. You can + /// later access this schema by calling GetMapping. + /// + /// + /// Whether the table was created or migrated. + /// + public Task CreateTableAsync (CreateFlags createFlags = CreateFlags.None) + where T : new() + { + return WriteAsync (conn => conn.CreateTable (createFlags)); + } + + /// + /// Executes a "create table if not exists" on the database. It also + /// creates any specified indexes on the columns of the table. It uses + /// a schema automatically generated from the specified type. You can + /// later access this schema by calling GetMapping. + /// + /// Type to reflect to a database table. + /// Optional flags allowing implicit PK and indexes based on naming conventions. + /// + /// Whether the table was created or migrated. + /// + public Task CreateTableAsync (Type ty, CreateFlags createFlags = CreateFlags.None) + { + return WriteAsync (conn => conn.CreateTable (ty, createFlags)); + } + + /// + /// Executes a "create table if not exists" on the database for each type. It also + /// creates any specified indexes on the columns of the table. It uses + /// a schema automatically generated from the specified type. You can + /// later access this schema by calling GetMapping. + /// + /// + /// Whether the table was created or migrated for each type. + /// public Task CreateTablesAsync (CreateFlags createFlags = CreateFlags.None) - where T : new () - where T2 : new () + where T : new() + where T2 : new() { return CreateTablesAsync (createFlags, typeof (T), typeof (T2)); } + /// + /// Executes a "create table if not exists" on the database for each type. It also + /// creates any specified indexes on the columns of the table. It uses + /// a schema automatically generated from the specified type. You can + /// later access this schema by calling GetMapping. + /// + /// + /// Whether the table was created or migrated for each type. + /// public Task CreateTablesAsync (CreateFlags createFlags = CreateFlags.None) - where T : new () - where T2 : new () - where T3 : new () + where T : new() + where T2 : new() + where T3 : new() { return CreateTablesAsync (createFlags, typeof (T), typeof (T2), typeof (T3)); } + /// + /// Executes a "create table if not exists" on the database for each type. It also + /// creates any specified indexes on the columns of the table. It uses + /// a schema automatically generated from the specified type. You can + /// later access this schema by calling GetMapping. + /// + /// + /// Whether the table was created or migrated for each type. + /// public Task CreateTablesAsync (CreateFlags createFlags = CreateFlags.None) - where T : new () - where T2 : new () - where T3 : new () - where T4 : new () + where T : new() + where T2 : new() + where T3 : new() + where T4 : new() { return CreateTablesAsync (createFlags, typeof (T), typeof (T2), typeof (T3), typeof (T4)); } + /// + /// Executes a "create table if not exists" on the database for each type. It also + /// creates any specified indexes on the columns of the table. It uses + /// a schema automatically generated from the specified type. You can + /// later access this schema by calling GetMapping. + /// + /// + /// Whether the table was created or migrated for each type. + /// public Task CreateTablesAsync (CreateFlags createFlags = CreateFlags.None) - where T : new () - where T2 : new () - where T3 : new () - where T4 : new () - where T5 : new () + where T : new() + where T2 : new() + where T3 : new() + where T4 : new() + where T5 : new() { return CreateTablesAsync (createFlags, typeof (T), typeof (T2), typeof (T3), typeof (T4), typeof (T5)); } + /// + /// Executes a "create table if not exists" on the database for each type. It also + /// creates any specified indexes on the columns of the table. It uses + /// a schema automatically generated from the specified type. You can + /// later access this schema by calling GetMapping. + /// + /// + /// Whether the table was created or migrated for each type. + /// public Task CreateTablesAsync (CreateFlags createFlags = CreateFlags.None, params Type[] types) { - return Task.Factory.StartNew (() => { - CreateTablesResult result = new CreateTablesResult (); - var conn = GetConnection (); - using (conn.Lock ()) { - foreach (Type type in types) { - int aResult = conn.CreateTable (type, createFlags); - result.Results[type] = aResult; - } - } - return result; - }); + return WriteAsync (conn => conn.CreateTables (createFlags, types)); } + /// + /// Executes a "drop table" on the database. This is non-recoverable. + /// public Task DropTableAsync () - where T : new () + where T : new() { - return Task.Factory.StartNew (() => { - var conn = GetConnection (); - using (conn.Lock ()) { - return conn.DropTable (); - } - }); + return WriteAsync (conn => conn.DropTable ()); } - public Task InsertAsync (object item) + /// + /// Executes a "drop table" on the database. This is non-recoverable. + /// + /// + /// The TableMapping used to identify the table. + /// + public Task DropTableAsync (TableMapping map) { - return Task.Factory.StartNew (() => { - var conn = GetConnection (); - using (conn.Lock ()) { - return conn.Insert (item); - } - }); + return WriteAsync (conn => conn.DropTable (map)); } - public Task InsertOrReplaceAsync(object item) - { - return Task.Factory.StartNew(() => - { - var conn = GetConnection(); - using (conn.Lock()) - { - return conn.InsertOrReplace(item); - } - }); - } + /// + /// Creates an index for the specified table and column. + /// + /// Name of the database table + /// Name of the column to index + /// Whether the index should be unique + public Task CreateIndexAsync (string tableName, string columnName, bool unique = false) + { + return WriteAsync (conn => conn.CreateIndex (tableName, columnName, unique)); + } - public Task UpdateAsync (object item) + /// + /// Creates an index for the specified table and column. + /// + /// Name of the index to create + /// Name of the database table + /// Name of the column to index + /// Whether the index should be unique + public Task CreateIndexAsync (string indexName, string tableName, string columnName, bool unique = false) { - return Task.Factory.StartNew (() => { - var conn = GetConnection (); - using (conn.Lock ()) { - return conn.Update (item); - } - }); + return WriteAsync (conn => conn.CreateIndex (indexName, tableName, columnName, unique)); } - public Task DeleteAsync (object item) + /// + /// Creates an index for the specified table and columns. + /// + /// Name of the database table + /// An array of column names to index + /// Whether the index should be unique + public Task CreateIndexAsync (string tableName, string[] columnNames, bool unique = false) { - return Task.Factory.StartNew (() => { - var conn = GetConnection (); - using (conn.Lock ()) { - return conn.Delete (item); - } - }); + return WriteAsync (conn => conn.CreateIndex (tableName, columnNames, unique)); } - public Task GetAsync(object pk) - where T : new() - { - return Task.Factory.StartNew(() => - { - var conn = GetConnection(); - using (conn.Lock()) - { - return conn.Get(pk); - } - }); - } + /// + /// Creates an index for the specified table and columns. + /// + /// Name of the index to create + /// Name of the database table + /// An array of column names to index + /// Whether the index should be unique + public Task CreateIndexAsync (string indexName, string tableName, string[] columnNames, bool unique = false) + { + return WriteAsync (conn => conn.CreateIndex (indexName, tableName, columnNames, unique)); + } - public Task FindAsync (object pk) - where T : new () + /// + /// Creates an index for the specified object property. + /// e.g. CreateIndex<Client>(c => c.Name); + /// + /// Type to reflect to a database table. + /// Property to index + /// Whether the index should be unique + public Task CreateIndexAsync (Expression> property, bool unique = false) { - return Task.Factory.StartNew (() => { - var conn = GetConnection (); - using (conn.Lock ()) { - return conn.Find (pk); - } - }); + return WriteAsync (conn => conn.CreateIndex (property, unique)); + } + + /// + /// Inserts the given object and retrieves its + /// auto incremented primary key if it has one. + /// + /// + /// The object to insert. + /// + /// + /// The number of rows added to the table. + /// + public Task InsertAsync (object obj) + { + return WriteAsync (conn => conn.Insert (obj)); + } + + /// + /// Inserts the given object (and updates its + /// auto incremented primary key if it has one). + /// The return value is the number of rows added to the table. + /// + /// + /// The object to insert. + /// + /// + /// The type of object to insert. + /// + /// + /// The number of rows added to the table. + /// + public Task InsertAsync (object obj, Type objType) + { + return WriteAsync (conn => conn.Insert (obj, objType)); + } + + /// + /// Inserts the given object (and updates its + /// auto incremented primary key if it has one). + /// The return value is the number of rows added to the table. + /// + /// + /// The object to insert. + /// + /// + /// Literal SQL code that gets placed into the command. INSERT {extra} INTO ... + /// + /// + /// The number of rows added to the table. + /// + public Task InsertAsync (object obj, string extra) + { + return WriteAsync (conn => conn.Insert (obj, extra)); + } + + /// + /// Inserts the given object (and updates its + /// auto incremented primary key if it has one). + /// The return value is the number of rows added to the table. + /// + /// + /// The object to insert. + /// + /// + /// Literal SQL code that gets placed into the command. INSERT {extra} INTO ... + /// + /// + /// The type of object to insert. + /// + /// + /// The number of rows added to the table. + /// + public Task InsertAsync (object obj, string extra, Type objType) + { + return WriteAsync (conn => conn.Insert (obj, extra, objType)); + } + + /// + /// Inserts the given object (and updates its + /// auto incremented primary key if it has one). + /// The return value is the number of rows added to the table. + /// If a UNIQUE constraint violation occurs with + /// some pre-existing object, this function deletes + /// the old object. + /// + /// + /// The object to insert. + /// + /// + /// The number of rows modified. + /// + public Task InsertOrReplaceAsync (object obj) + { + return WriteAsync (conn => conn.InsertOrReplace (obj)); + } + + /// + /// Inserts the given object (and updates its + /// auto incremented primary key if it has one). + /// The return value is the number of rows added to the table. + /// If a UNIQUE constraint violation occurs with + /// some pre-existing object, this function deletes + /// the old object. + /// + /// + /// The object to insert. + /// + /// + /// The type of object to insert. + /// + /// + /// The number of rows modified. + /// + public Task InsertOrReplaceAsync (object obj, Type objType) + { + return WriteAsync (conn => conn.InsertOrReplace (obj, objType)); + } + + /// + /// Updates all of the columns of a table using the specified object + /// except for its primary key. + /// The object is required to have a primary key. + /// + /// + /// The object to update. It must have a primary key designated using the PrimaryKeyAttribute. + /// + /// + /// The number of rows updated. + /// + public Task UpdateAsync (object obj) + { + return WriteAsync (conn => conn.Update (obj)); + } + + /// + /// Updates all of the columns of a table using the specified object + /// except for its primary key. + /// The object is required to have a primary key. + /// + /// + /// The object to update. It must have a primary key designated using the PrimaryKeyAttribute. + /// + /// + /// The type of object to insert. + /// + /// + /// The number of rows updated. + /// + public Task UpdateAsync (object obj, Type objType) + { + return WriteAsync (conn => conn.Update (obj, objType)); + } + + /// + /// Updates all specified objects. + /// + /// + /// An of the objects to insert. + /// + /// + /// A boolean indicating if the inserts should be wrapped in a transaction + /// + /// + /// The number of rows modified. + /// + public Task UpdateAllAsync (IEnumerable objects, bool runInTransaction = true) + { + return WriteAsync (conn => conn.UpdateAll (objects, runInTransaction)); + } + + /// + /// Deletes the given object from the database using its primary key. + /// + /// + /// The object to delete. It must have a primary key designated using the PrimaryKeyAttribute. + /// + /// + /// The number of rows deleted. + /// + public Task DeleteAsync (object objectToDelete) + { + return WriteAsync (conn => conn.Delete (objectToDelete)); + } + + /// + /// Deletes the object with the specified primary key. + /// + /// + /// The primary key of the object to delete. + /// + /// + /// The number of objects deleted. + /// + /// + /// The type of object. + /// + public Task DeleteAsync (object primaryKey) + { + return WriteAsync (conn => conn.Delete (primaryKey)); + } + + /// + /// Deletes the object with the specified primary key. + /// + /// + /// The primary key of the object to delete. + /// + /// + /// The TableMapping used to identify the table. + /// + /// + /// The number of objects deleted. + /// + public Task DeleteAsync (object primaryKey, TableMapping map) + { + return WriteAsync (conn => conn.Delete (primaryKey, map)); + } + + /// + /// Deletes all the objects from the specified table. + /// WARNING WARNING: Let me repeat. It deletes ALL the objects from the + /// specified table. Do you really want to do that? + /// + /// + /// The number of objects deleted. + /// + /// + /// The type of objects to delete. + /// + public Task DeleteAllAsync () + { + return WriteAsync (conn => conn.DeleteAll ()); + } + + /// + /// Deletes all the objects from the specified table. + /// WARNING WARNING: Let me repeat. It deletes ALL the objects from the + /// specified table. Do you really want to do that? + /// + /// + /// The TableMapping used to identify the table. + /// + /// + /// The number of objects deleted. + /// + public Task DeleteAllAsync (TableMapping map) + { + return WriteAsync (conn => conn.DeleteAll (map)); + } + + /// + /// Attempts to retrieve an object with the given primary key from the table + /// associated with the specified type. Use of this method requires that + /// the given type have a designated PrimaryKey (using the PrimaryKeyAttribute). + /// + /// + /// The primary key. + /// + /// + /// The object with the given primary key. Throws a not found exception + /// if the object is not found. + /// + public Task GetAsync (object pk) + where T : new() + { + return ReadAsync (conn => conn.Get (pk)); } - + + /// + /// Attempts to retrieve an object with the given primary key from the table + /// associated with the specified type. Use of this method requires that + /// the given type have a designated PrimaryKey (using the PrimaryKeyAttribute). + /// + /// + /// The primary key. + /// + /// + /// The TableMapping used to identify the table. + /// + /// + /// The object with the given primary key. Throws a not found exception + /// if the object is not found. + /// + public Task GetAsync (object pk, TableMapping map) + { + return ReadAsync (conn => conn.Get (pk, map)); + } + + /// + /// Attempts to retrieve the first object that matches the predicate from the table + /// associated with the specified type. + /// + /// + /// A predicate for which object to find. + /// + /// + /// The object that matches the given predicate. Throws a not found exception + /// if the object is not found. + /// public Task GetAsync (Expression> predicate) - where T : new() - { - return Task.Factory.StartNew(() => - { - var conn = GetConnection(); - using (conn.Lock()) - { - return conn.Get (predicate); - } - }); - } + where T : new() + { + return ReadAsync (conn => conn.Get (predicate)); + } + /// + /// Attempts to retrieve an object with the given primary key from the table + /// associated with the specified type. Use of this method requires that + /// the given type have a designated PrimaryKey (using the PrimaryKeyAttribute). + /// + /// + /// The primary key. + /// + /// + /// The object with the given primary key or null + /// if the object is not found. + /// + public Task FindAsync (object pk) + where T : new() + { + return ReadAsync (conn => conn.Find (pk)); + } + + /// + /// Attempts to retrieve an object with the given primary key from the table + /// associated with the specified type. Use of this method requires that + /// the given type have a designated PrimaryKey (using the PrimaryKeyAttribute). + /// + /// + /// The primary key. + /// + /// + /// The TableMapping used to identify the table. + /// + /// + /// The object with the given primary key or null + /// if the object is not found. + /// + public Task FindAsync (object pk, TableMapping map) + { + return ReadAsync (conn => conn.Find (pk, map)); + } + + /// + /// Attempts to retrieve the first object that matches the predicate from the table + /// associated with the specified type. + /// + /// + /// A predicate for which object to find. + /// + /// + /// The object that matches the given predicate or null + /// if the object is not found. + /// public Task FindAsync (Expression> predicate) - where T : new () + where T : new() { - return Task.Factory.StartNew (() => { - var conn = GetConnection (); - using (conn.Lock ()) { - return conn.Find (predicate); - } - }); + return ReadAsync (conn => conn.Find (predicate)); } + /// + /// Attempts to retrieve the first object that matches the query from the table + /// associated with the specified type. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// The object that matches the given predicate or null + /// if the object is not found. + /// + public Task FindWithQueryAsync (string query, params object[] args) + where T : new() + { + return ReadAsync (conn => conn.FindWithQuery (query, args)); + } + + /// + /// Attempts to retrieve the first object that matches the query from the table + /// associated with the specified type. + /// + /// + /// The TableMapping used to identify the table. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// The object that matches the given predicate or null + /// if the object is not found. + /// + public Task FindWithQueryAsync (TableMapping map, string query, params object[] args) + { + return ReadAsync (conn => conn.FindWithQuery (map, query, args)); + } + + /// + /// Retrieves the mapping that is automatically generated for the given type. + /// + /// + /// The type whose mapping to the database is returned. + /// + /// + /// Optional flags allowing implicit PK and indexes based on naming conventions + /// + /// + /// The mapping represents the schema of the columns of the database and contains + /// methods to set and get properties of objects. + /// + public Task GetMappingAsync (Type type, CreateFlags createFlags = CreateFlags.None) + { + return ReadAsync (conn => conn.GetMapping (type, createFlags)); + } + + /// + /// Retrieves the mapping that is automatically generated for the given type. + /// + /// + /// Optional flags allowing implicit PK and indexes based on naming conventions + /// + /// + /// The mapping represents the schema of the columns of the database and contains + /// methods to set and get properties of objects. + /// + public Task GetMappingAsync (CreateFlags createFlags = CreateFlags.None) + where T : new() + { + return ReadAsync (conn => conn.GetMapping (createFlags)); + } + + /// + /// Query the built-in sqlite table_info table for a specific tables columns. + /// + /// The columns contains in the table. + /// Table name. + public Task> GetTableInfoAsync (string tableName) + { + return ReadAsync (conn => conn.GetTableInfo (tableName)); + } + + /// + /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' + /// in the command text for each of the arguments and then executes that command. + /// Use this method instead of Query when you don't expect rows back. Such cases include + /// INSERTs, UPDATEs, and DELETEs. + /// You can set the Trace or TimeExecution properties of the connection + /// to profile execution. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// The number of rows modified in the database as a result of this execution. + /// public Task ExecuteAsync (string query, params object[] args) { - return Task.Factory.StartNew (() => { - var conn = GetConnection (); - using (conn.Lock ()) { - return conn.Execute (query, args); - } - }); + return WriteAsync (conn => conn.Execute (query, args)); } - public Task InsertAllAsync (IEnumerable items) + /// + /// Inserts all specified objects. + /// + /// + /// An of the objects to insert. + /// + /// A boolean indicating if the inserts should be wrapped in a transaction. + /// + /// + /// The number of rows added to the table. + /// + public Task InsertAllAsync (IEnumerable objects, bool runInTransaction = true) { - return Task.Factory.StartNew (() => { - var conn = GetConnection (); - using (conn.Lock ()) { - return conn.InsertAll (items); - } - }); + return WriteAsync (conn => conn.InsertAll (objects, runInTransaction)); } - - public Task UpdateAllAsync (IEnumerable items) + + /// + /// Inserts all specified objects. + /// + /// + /// An of the objects to insert. + /// + /// + /// Literal SQL code that gets placed into the command. INSERT {extra} INTO ... + /// + /// + /// A boolean indicating if the inserts should be wrapped in a transaction. + /// + /// + /// The number of rows added to the table. + /// + public Task InsertAllAsync (IEnumerable objects, string extra, bool runInTransaction = true) { - return Task.Factory.StartNew (() => { - var conn = GetConnection (); - using (conn.Lock ()) { - return conn.UpdateAll (items); - } - }); + return WriteAsync (conn => conn.InsertAll (objects, extra, runInTransaction)); } - [Obsolete("Will cause a deadlock if any call in action ends up in a different thread. Use RunInTransactionAsync(Action) instead.")] - public Task RunInTransactionAsync (Action action) + /// + /// Inserts all specified objects. + /// + /// + /// An of the objects to insert. + /// + /// + /// The type of object to insert. + /// + /// + /// A boolean indicating if the inserts should be wrapped in a transaction. + /// + /// + /// The number of rows added to the table. + /// + public Task InsertAllAsync (IEnumerable objects, Type objType, bool runInTransaction = true) { - return Task.Factory.StartNew (() => { - var conn = this.GetConnection (); - using (conn.Lock ()) { - conn.BeginTransaction (); - try { - action (this); - conn.Commit (); - } - catch (Exception) { - conn.Rollback (); - throw; - } + return WriteAsync (conn => conn.InsertAll (objects, objType, runInTransaction)); + } + + /// + /// Executes within a (possibly nested) transaction by wrapping it in a SAVEPOINT. If an + /// exception occurs the whole transaction is rolled back, not just the current savepoint. The exception + /// is rethrown. + /// + /// + /// The to perform within a transaction. can contain any number + /// of operations on the connection but should never call or + /// . + /// + public Task RunInTransactionAsync (Action action) + { + return WriteAsync (conn => { + conn.BeginTransaction (); + try { + action (conn); + conn.Commit (); + return null; + } + catch (Exception) { + conn.Rollback (); + throw; } }); } - public Task RunInTransactionAsync(Action action) - { - return Task.Factory.StartNew(() => - { - var conn = this.GetConnection(); - using (conn.Lock()) - { - conn.BeginTransaction(); - try - { - action(conn); - conn.Commit(); - } - catch (Exception) - { - conn.Rollback(); - throw; - } - } - }); - } - + /// + /// Returns a queryable interface to the table represented by the given type. + /// + /// + /// A queryable object that is able to translate Where, OrderBy, and Take + /// queries into native SQL. + /// public AsyncTableQuery Table () - where T : new () + where T : new() { // // This isn't async as the underlying connection doesn't go out to the database @@ -296,26 +1006,124 @@ public AsyncTableQuery Table () return new AsyncTableQuery (conn.Table ()); } - public Task ExecuteScalarAsync (string sql, params object[] args) + /// + /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' + /// in the command text for each of the arguments and then executes that command. + /// Use this method when return primitive values. + /// You can set the Trace or TimeExecution properties of the connection + /// to profile execution. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// The number of rows modified in the database as a result of this execution. + /// + public Task ExecuteScalarAsync (string query, params object[] args) { - return Task.Factory.StartNew (() => { - var conn = GetConnection (); - using (conn.Lock ()) { - var command = conn.CreateCommand (sql, args); - return command.ExecuteScalar (); - } + return WriteAsync (conn => { + var command = conn.CreateCommand (query, args); + return command.ExecuteScalar (); }); } - public Task> QueryAsync (string sql, params object[] args) - where T : new () + /// + /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' + /// in the command text for each of the arguments and then executes that command. + /// It returns each row of the result using the mapping automatically generated for + /// the given type. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// An enumerable with one result for each row returned by the query. + /// + public Task> QueryAsync (string query, params object[] args) + where T : new() { - return Task>.Factory.StartNew (() => { - var conn = GetConnection (); - using (conn.Lock ()) { - return conn.Query (sql, args); - } - }); + return ReadAsync (conn => conn.Query (query, args)); + } + + /// + /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' + /// in the command text for each of the arguments and then executes that command. + /// It returns each row of the result using the specified mapping. This function is + /// only used by libraries in order to query the database via introspection. It is + /// normally not used. + /// + /// + /// A to use to convert the resulting rows + /// into objects. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// An enumerable with one result for each row returned by the query. + /// + public Task> QueryAsync (TableMapping map, string query, params object[] args) + { + return ReadAsync (conn => conn.Query (map, query, args)); + } + + /// + /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' + /// in the command text for each of the arguments and then executes that command. + /// It returns each row of the result using the mapping automatically generated for + /// the given type. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// An enumerable with one result for each row returned by the query. + /// The enumerator will call sqlite3_step on each call to MoveNext, so the database + /// connection must remain open for the lifetime of the enumerator. + /// + public Task> DeferredQueryAsync (string query, params object[] args) + where T : new() + { + return ReadAsync (conn => (IEnumerable)conn.DeferredQuery (query, args).ToList ()); + } + + /// + /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' + /// in the command text for each of the arguments and then executes that command. + /// It returns each row of the result using the specified mapping. This function is + /// only used by libraries in order to query the database via introspection. It is + /// normally not used. + /// + /// + /// A to use to convert the resulting rows + /// into objects. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// An enumerable with one result for each row returned by the query. + /// The enumerator will call sqlite3_step on each call to MoveNext, so the database + /// connection must remain open for the lifetime of the enumerator. + /// + public Task> DeferredQueryAsync (TableMapping map, string query, params object[] args) + { + return ReadAsync (conn => (IEnumerable)conn.DeferredQuery (map, query, args).ToList ()); } } @@ -323,94 +1131,185 @@ public Task> QueryAsync (string sql, params object[] args) // TODO: Bind to AsyncConnection.GetConnection instead so that delayed // execution can still work after a Pool.Reset. // + + /// + /// Query to an asynchronous database connection. + /// public class AsyncTableQuery - where T : new () + where T : new() { TableQuery _innerQuery; + /// + /// Creates a new async query that uses given the synchronous query. + /// public AsyncTableQuery (TableQuery innerQuery) { _innerQuery = innerQuery; } + Task ReadAsync (Func read) + { + return Task.Factory.StartNew (() => { + var conn = (SQLiteConnectionWithLock)_innerQuery.Connection; + using (conn.Lock ()) { + return read (conn); + } + }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + } + + Task WriteAsync (Func write) + { + return Task.Factory.StartNew (() => { + var conn = (SQLiteConnectionWithLock)_innerQuery.Connection; + using (conn.Lock ()) { + return write (conn); + } + }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + } + + /// + /// Filters the query based on a predicate. + /// public AsyncTableQuery Where (Expression> predExpr) { return new AsyncTableQuery (_innerQuery.Where (predExpr)); } + /// + /// Skips a given number of elements from the query and then yields the remainder. + /// public AsyncTableQuery Skip (int n) { return new AsyncTableQuery (_innerQuery.Skip (n)); } + /// + /// Yields a given number of elements from the query and then skips the remainder. + /// public AsyncTableQuery Take (int n) { return new AsyncTableQuery (_innerQuery.Take (n)); } + /// + /// Order the query results according to a key. + /// public AsyncTableQuery OrderBy (Expression> orderExpr) { return new AsyncTableQuery (_innerQuery.OrderBy (orderExpr)); } + /// + /// Order the query results according to a key. + /// public AsyncTableQuery OrderByDescending (Expression> orderExpr) { return new AsyncTableQuery (_innerQuery.OrderByDescending (orderExpr)); } + /// + /// Order the query results according to a key. + /// + public AsyncTableQuery ThenBy (Expression> orderExpr) + { + return new AsyncTableQuery (_innerQuery.ThenBy (orderExpr)); + } + + /// + /// Order the query results according to a key. + /// + public AsyncTableQuery ThenByDescending (Expression> orderExpr) + { + return new AsyncTableQuery (_innerQuery.ThenByDescending (orderExpr)); + } + + /// + /// Queries the database and returns the results as a List. + /// public Task> ToListAsync () { - return Task.Factory.StartNew (() => { - using (((SQLiteConnectionWithLock)_innerQuery.Connection).Lock ()) { - return _innerQuery.ToList (); - } - }); + return ReadAsync (conn => _innerQuery.ToList ()); + } + + /// + /// Queries the database and returns the results as an array. + /// + public Task ToArrayAsync () + { + return ReadAsync (conn => _innerQuery.ToArray ()); } + /// + /// Execute SELECT COUNT(*) on the query + /// public Task CountAsync () { - return Task.Factory.StartNew (() => { - using (((SQLiteConnectionWithLock)_innerQuery.Connection).Lock ()) { - return _innerQuery.Count (); - } - }); + return ReadAsync (conn => _innerQuery.Count ()); + } + + /// + /// Execute SELECT COUNT(*) on the query with an additional WHERE clause. + /// + public Task CountAsync (Expression> predExpr) + { + return ReadAsync (conn => _innerQuery.Count (predExpr)); } + /// + /// Returns the element at a given index + /// public Task ElementAtAsync (int index) { - return Task.Factory.StartNew (() => { - using (((SQLiteConnectionWithLock)_innerQuery.Connection).Lock ()) { - return _innerQuery.ElementAt (index); - } - }); + return ReadAsync (conn => _innerQuery.ElementAt (index)); } + /// + /// Returns the first element of this query. + /// public Task FirstAsync () { - return Task.Factory.StartNew(() => { - using (((SQLiteConnectionWithLock)_innerQuery.Connection).Lock ()) { - return _innerQuery.First (); - } - }); + return ReadAsync (conn => _innerQuery.First ()); } + /// + /// Returns the first element of this query, or null if no element is found. + /// public Task FirstOrDefaultAsync () { - return Task.Factory.StartNew(() => { - using (((SQLiteConnectionWithLock)_innerQuery.Connection).Lock ()) { - return _innerQuery.FirstOrDefault (); - } - }); + return ReadAsync (conn => _innerQuery.FirstOrDefault ()); } - } - public class CreateTablesResult - { - public Dictionary Results { get; private set; } + /// + /// Returns the first element of this query that matches the predicate. + /// + public Task FirstAsync (Expression> predExpr) + { + return ReadAsync (conn => _innerQuery.First (predExpr)); + } + + /// + /// Returns the first element of this query that matches the predicate. + /// + public Task FirstOrDefaultAsync (Expression> predExpr) + { + return ReadAsync (conn => _innerQuery.FirstOrDefault (predExpr)); + } + + /// + /// Delete all the rows that match this query and the given predicate. + /// + public Task DeleteAsync (Expression> predExpr) + { + return WriteAsync (conn => _innerQuery.Delete (predExpr)); + } - internal CreateTablesResult () + /// + /// Delete all the rows that match this query. + /// + public Task DeleteAsync () { - this.Results = new Dictionary (); + return WriteAsync (conn => _innerQuery.Delete ()); } } @@ -421,15 +1320,19 @@ class Entry public SQLiteConnectionString ConnectionString { get; private set; } public SQLiteConnectionWithLock Connection { get; private set; } - public Entry (SQLiteConnectionString connectionString, SQLiteOpenFlags openFlags) + public Entry (SQLiteConnectionString connectionString, SQLiteOpenFlags openFlags) { ConnectionString = connectionString; Connection = new SQLiteConnectionWithLock (connectionString, openFlags); } - public void OnApplicationSuspended () + public void Close () { - Connection.Dispose (); + if (Connection == null) + return; + using (var l = Connection.Lock ()) { + Connection.Dispose (); + } Connection = null; } } @@ -442,10 +1345,8 @@ public void OnApplicationSuspended () /// /// Gets the singleton instance of the connection tool. /// - public static SQLiteConnectionPool Shared - { - get - { + public static SQLiteConnectionPool Shared { + get { return _shared; } } @@ -465,44 +1366,72 @@ public SQLiteConnectionWithLock GetConnection (SQLiteConnectionString connection } } - /// - /// Closes all connections managed by this pool. - /// - public void Reset () + public void CloseConnection (SQLiteConnectionString connectionString, SQLiteOpenFlags openFlags) { + var key = connectionString.ConnectionString; + + Entry entry; lock (_entriesLock) { - foreach (var entry in _entries.Values) { - entry.OnApplicationSuspended (); + if (_entries.TryGetValue (key, out entry)) { + _entries.Remove (key); } - _entries.Clear (); } + + entry.Close (); } /// - /// Call this method when the application is suspended. + /// Closes all connections managed by this pool. /// - /// Behaviour here is to close any open connections. - public void ApplicationSuspended () + public void Reset () { - Reset (); + List entries; + lock (_entriesLock) { + entries = new List (_entries.Values); + _entries.Clear (); + } + + foreach (var e in entries) { + e.Close (); + } } } + /// + /// This is a normal connection except it contains a Lock method that + /// can be used to serialize access to the database. + /// public class SQLiteConnectionWithLock : SQLiteConnection { readonly object _lockPoint = new object (); - public SQLiteConnectionWithLock (SQLiteConnectionString connectionString, SQLiteOpenFlags openFlags) + /// + /// Initializes a new instance of the class. + /// + /// Connection string containing the DatabasePath. + /// Open flags. + public SQLiteConnectionWithLock (SQLiteConnectionString connectionString, SQLiteOpenFlags openFlags) : base (connectionString.DatabasePath, openFlags, connectionString.StoreDateTimeAsTicks) { } + /// + /// Gets or sets a value indicating whether this skip lock. + /// + /// true if skip lock; otherwise, false. + public bool SkipLock { get; set; } + + /// + /// Lock the database to serialize access to it. To unlock it, call Dispose + /// on the returned object. + /// + /// The lock. public IDisposable Lock () { - return new LockWrapper (_lockPoint); + return SkipLock ? (IDisposable)new FakeLockWrapper() : new LockWrapper (_lockPoint); } - private class LockWrapper : IDisposable + class LockWrapper : IDisposable { object _lockPoint; @@ -517,6 +1446,12 @@ public void Dispose () Monitor.Exit (_lockPoint); } } + class FakeLockWrapper : IDisposable + { + public void Dispose () + { + } + } } } diff --git a/tests/ApiDiff/ApiDiff.csproj b/tests/ApiDiff/ApiDiff.csproj new file mode 100644 index 000000000..577d53938 --- /dev/null +++ b/tests/ApiDiff/ApiDiff.csproj @@ -0,0 +1,51 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {1DEF735C-B973-4ED9-8446-7FFA6D0B410B} + Exe + ApiDiff + ApiDiff + v4.6.1 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + true + + + true + bin\Release + prompt + 4 + true + + + + + ..\..\packages\ListDiff.1.0.7\lib\netstandard1.0\ListDiff.dll + + + + + + + SQLite.cs + + + SQLiteAsync.cs + + + + + + + \ No newline at end of file diff --git a/tests/ApiDiff/Program.cs b/tests/ApiDiff/Program.cs new file mode 100644 index 000000000..b0679103f --- /dev/null +++ b/tests/ApiDiff/Program.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Linq; +using System.Text.RegularExpressions; +using SQLite; +using ListDiff; + +namespace ApiDiff +{ + class Api + { + public string Name; + public string Declaration; + public string Index; + + static readonly Regex task1Re = new Regex (@"System\.Threading\.Tasks\.Task`1\[([^\]]*)\]"); + static readonly Regex taskRe = new Regex (@"System\.Threading\.Tasks\.Task(\s*)"); + + public Api (MemberInfo member, string nameSuffix) + { + Name = member.Name; + Declaration = member.ToString (); + Index = Declaration.Replace ("AsyncTableQuery`1", "TableQuery`1"); + + if (nameSuffix.Length > 0 && Name.EndsWith (nameSuffix)) { + var indexName = Name.Substring (0, Name.IndexOf (nameSuffix)); + Index = taskRe + .Replace (task1Re.Replace (Index.Replace (Name, indexName), "$1"), "Void$1") + .Replace ("System.Int32", "Int32"); + Name = indexName; + } + } + } + + class Apis + { + public List All; + readonly string nameSuffix; + readonly Type type; + + public static readonly HashSet connectionIgnores = new HashSet { + "RunInTransaction", + "RunInTransactionAsync", + "BeginTransaction", + "SaveTransactionPoint", + "Commit", + "Rollback", + "RollbackTo", + "IsInTransaction", + "Release", + "EndTransaction", + + "BusyTimeout", + "GetBusyTimeout", + "SetBusyTimeoutAsync", + + "GetConnection", + "Handle", + + "Dispose", + + "Table", + "CreateCommand", + "TableChanged", + }; + + public static readonly HashSet queryIgnores = new HashSet { + ".ctor", + "Clone", + "Connection", + "Deferred", + "Table", + "GetEnumerator", + }; + + public Apis (Type type, HashSet ignores, string nameSuffix = "") + { + this.type = type; + this.nameSuffix = nameSuffix; + All = type.GetMembers (BindingFlags.Public|BindingFlags.Instance) + .Where (x => !ignores.Contains(x.Name)) + .Where (x => x.MemberType != MemberTypes.NestedType) + .Where (x => !x.Name.StartsWith("get_") && !x.Name.StartsWith ("set_") && !x.Name.StartsWith ("remove_") && !x.Name.StartsWith ("add_")) + .Select (x => new Api(x, nameSuffix)) + .OrderBy (x => x.Index) + .OrderBy (x => x.Name) + .ToList (); + } + + public int DumpComparison (Apis other) + { + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine ("## " + type.FullName); + Console.WriteLine (); + + var diff = new ListDiff (All, other.All, (x, y) => x.Index == y.Index); + + var n = 0; + + foreach (var a in diff.Actions) { + switch (a.ActionType) { + case ListDiffActionType.Add: + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine ($"- [ ] *add* `{a.DestinationItem.Index.Replace('`', '_')}`"); + n++; + break; + case ListDiffActionType.Remove: + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine ($"- [ ] *remove* `{a.SourceItem.Index.Replace('`', '_')}`"); + n++; + break; + case ListDiffActionType.Update: + Console.ForegroundColor = ConsoleColor.Gray; + Console.WriteLine ($"- [x] `{a.SourceItem.Index.Replace('`', '_')}`"); + break; + } + } + Console.ResetColor (); + Console.WriteLine (); + Console.WriteLine ($"**{n}** differences"); + Console.WriteLine (); + + return n; + } + } + + class MainClass + { + public static int Main (string[] args) + { + var synchronousConnection = new Apis (typeof (SQLiteConnection), Apis.connectionIgnores); + var asynchronousConnection = new Apis (typeof (SQLiteAsyncConnection), Apis.connectionIgnores, "Async"); + var n = asynchronousConnection.DumpComparison (synchronousConnection); + + var synchronousQuery = new Apis (typeof (TableQuery<>), Apis.queryIgnores); + var asynchronousQuery = new Apis (typeof (AsyncTableQuery<>), Apis.queryIgnores, "Async"); + n += asynchronousQuery.DumpComparison (synchronousQuery); + + return n > 0 ? 1 : 0; + } + } +} diff --git a/tests/ApiDiff/Properties/AssemblyInfo.cs b/tests/ApiDiff/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..6549154de --- /dev/null +++ b/tests/ApiDiff/Properties/AssemblyInfo.cs @@ -0,0 +1,26 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. + +[assembly: AssemblyTitle ("ApiDiff")] +[assembly: AssemblyDescription ("")] +[assembly: AssemblyConfiguration ("")] +[assembly: AssemblyCompany ("")] +[assembly: AssemblyProduct ("")] +[assembly: AssemblyCopyright ("${AuthorCopyright}")] +[assembly: AssemblyTrademark ("")] +[assembly: AssemblyCulture ("")] + +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. + +[assembly: AssemblyVersion ("1.0.*")] + +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] diff --git a/tests/ApiDiff/packages.config b/tests/ApiDiff/packages.config new file mode 100644 index 000000000..48073015f --- /dev/null +++ b/tests/ApiDiff/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/tests/AsyncTests.cs b/tests/AsyncTests.cs index 15fd2b751..c76b231fd 100644 --- a/tests/AsyncTests.cs +++ b/tests/AsyncTests.cs @@ -31,7 +31,7 @@ public class Customer [MaxLength (64)] public string LastName { get; set; } - [MaxLength (64)] + [MaxLength (64), Indexed] public string Email { get; set; } } @@ -100,6 +100,10 @@ public void StressAsync () doneEvent.WaitOne (); var count = globalConn.Table ().CountAsync ().Result; + + foreach (var e in errors) { + Console.WriteLine ("ERROR " + e); + } Assert.AreEqual (0, errors.Count); Assert.AreEqual (n, count); @@ -817,5 +821,43 @@ public void TestAsyncGetWithExpression() // check... Assert.AreEqual("7", loaded.FirstName); } + + [Test] + public void CreateTable () + { + var conn = GetConnection (); + + var trace = new List (); + conn.Tracer = trace.Add; + conn.Trace = true; + + var r0 = conn.CreateTableAsync ().Result; + + Assert.AreEqual (CreateTableResult.Created, r0); + + var r1 = conn.CreateTableAsync ().Result; + + Assert.AreEqual (CreateTableResult.Migrated, r1); + + var r2 = conn.CreateTableAsync ().Result; + + Assert.AreEqual (CreateTableResult.Migrated, r1); + + Assert.AreEqual (7, trace.Count); + } + + [Test] + public void CloseAsync () + { + var conn = GetConnection (); + + var r0 = conn.CreateTableAsync ().Result; + + Assert.AreEqual (CreateTableResult.Created, r0); + + conn.CloseAsync ().Wait (); + } + + } } diff --git a/tests/ConcurrencyTest.cs b/tests/ConcurrencyTest.cs new file mode 100644 index 000000000..358a11330 --- /dev/null +++ b/tests/ConcurrencyTest.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.IO; + +#if NETFX_CORE +using Microsoft.VisualStudio.TestPlatform.UnitTestFramework; +using SetUp = Microsoft.VisualStudio.TestPlatform.UnitTestFramework.TestInitializeAttribute; +using TearDown = Microsoft.VisualStudio.TestPlatform.UnitTestFramework.TestCleanupAttribute; +using TestFixture = Microsoft.VisualStudio.TestPlatform.UnitTestFramework.TestClassAttribute; +using Test = Microsoft.VisualStudio.TestPlatform.UnitTestFramework.TestMethodAttribute; +#else +using NUnit.Framework; +#endif + +namespace SQLite.Tests +{ + [TestFixture] + public class ConcurrencyTest + { + public class TestObj + { + [AutoIncrement, PrimaryKey] + public int Id { get; set; } + + public override string ToString() + { + return string.Format("[TestObj: Id={0}]", Id); + } + } + + public class DbReader + { + private CancellationToken cancellationToken; + + public DbReader(CancellationToken cancellationToken) + { + this.cancellationToken = cancellationToken; + } + + public Task Run() + { + var t = Task.Run(() => + { + try + { + while (true) + { + // + // NOTE: Change this to readwrite and then it does work ??? + // No more IOERROR + // + + var flags = SQLiteOpenFlags.FullMutex | SQLiteOpenFlags.ReadOnly; +#if __IOS__ + flags = SQLiteOpenFlags.FullMutex | SQLiteOpenFlags.ReadWrite; +#endif + using (var dbConnection = new DbConnection(flags)) + { + var records = dbConnection.Table().ToList(); + System.Diagnostics.Debug.WriteLine($"{Environment.CurrentManagedThreadId} Read records: {records.Count}"); + } + + // No await so we stay on the same thread + Task.Delay(10).GetAwaiter().GetResult(); + cancellationToken.ThrowIfCancellationRequested(); + } + } + catch (OperationCanceledException) + { + } + }); + + return t; + } + + } + + public class DbWriter + { + private CancellationToken cancellationToken; + + public DbWriter(CancellationToken cancellationToken) + { + this.cancellationToken = cancellationToken; + } + + public Task Run() + { + var t = Task.Run(() => + { + try + { + while (true) + { + using (var dbConnection = new DbConnection(SQLiteOpenFlags.FullMutex | SQLiteOpenFlags.ReadWrite)) + { + System.Diagnostics.Debug.WriteLine($"{Environment.CurrentManagedThreadId} Start insert"); + + for (var i = 0; i < 50; i++) + { + var newRecord = new TestObj() + { + }; + + dbConnection.Insert(newRecord); + } + + System.Diagnostics.Debug.WriteLine($"{Environment.CurrentManagedThreadId} Inserted records"); + } + + // No await so we stay on the same thread + Task.Delay(1).GetAwaiter().GetResult(); + cancellationToken.ThrowIfCancellationRequested(); + } + } + catch (OperationCanceledException) + { + } + }); + + return t; + } + + } + + public class DbConnection : SQLiteConnection + { + private static string DbPath = GetTempFileName(); + + private static string GetTempFileName() + { +#if NETFX_CORE + var name = Guid.NewGuid() + ".sqlite"; + return Path.Combine(Windows.Storage.ApplicationData.Current.LocalFolder.Path, name); +#else + return Path.GetTempFileName(); +#endif + } + + + public DbConnection(SQLiteOpenFlags openflags) : base(DbPath, openflags) + { + this.BusyTimeout = TimeSpan.FromSeconds(5); + } + + public void CreateTables() + { + CreateTable(); + } + } + + [SetUp] + public void Setup() + { + using (var dbConenction = new DbConnection(SQLiteOpenFlags.FullMutex | SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create)) + { + dbConenction.CreateTables(); + } + } + + + [Test] + public void TestLoad() + { + try + { + //var result = SQLitePCL.raw.sqlite3_threadsafe(); + //Assert.AreEqual(2, result); + // Yes it's threadsafe on iOS + + var tokenSource = new CancellationTokenSource(); + var tasks = new List(); + tasks.Add(new DbReader(tokenSource.Token).Run()); + tasks.Add(new DbWriter(tokenSource.Token).Run()); + + // Wait 5sec + tokenSource.CancelAfter(5000); + + Task.WhenAll(tasks).GetAwaiter().GetResult(); + } + catch (Exception ex) + { + Assert.Fail(ex.ToString()); + } + } + + + } +} + diff --git a/tests/CreateTableTest.cs b/tests/CreateTableTest.cs index 1b16f0eaa..74d12eb2a 100644 --- a/tests/CreateTableTest.cs +++ b/tests/CreateTableTest.cs @@ -107,5 +107,47 @@ public void Issue115_MissingPrimaryKey () } } } + + [Table("WantsNoRowId", WithoutRowId = true)] + class WantsNoRowId + { + [PrimaryKey] + public int Id { get; set; } + public string Name { get; set; } + } + + [Table("sqlite_master")] + class SqliteMaster + { + [Column ("type")] + public string Type { get; set; } + + [Column ("name")] + public string Name { get; set; } + + [Column ("tbl_name")] + public string TableName { get; set; } + + [Column ("rootpage")] + public int RootPage { get; set; } + + [Column ("sql")] + public string Sql { get; set; } + } + + [Test] + public void WithoutRowId () + { + using(var conn = new TestDb ()) + { + conn.CreateTable (); + var info = conn.Table().Where(m => m.TableName=="OrderLine").First (); + Assert.That (!info.Sql.Contains ("without rowid")); + + conn.CreateTable (); + info = conn.Table().Where(m => m.TableName=="WantsNoRowId").First (); + Assert.That (info.Sql.Contains ("without rowid")); + } + } } } diff --git a/tests/DeleteTest.cs b/tests/DeleteTest.cs index 81a42d88e..ac77cfa93 100644 --- a/tests/DeleteTest.cs +++ b/tests/DeleteTest.cs @@ -81,27 +81,77 @@ public void DeleteAll () } [Test] - public void DeleteAllWithPredicate() + public void DeleteWithPredicate() { var db = CreateDb(); - var r = db.Table().Delete(p => p.Test == "Hello World"); + var r = db.Table().Delete (p => p.Test == "Hello World"); Assert.AreEqual (Count, r); Assert.AreEqual (0, db.Table ().Count ()); } [Test] - public void DeleteAllWithPredicateHalf() + public void DeleteWithPredicateHalf() { var db = CreateDb(); - db.Insert(new TestTable() { Datum = 1, Test = "Hello World 2" }); + db.Insert(new TestTable() { Datum = 1, Test = "Hello World 2" }); - var r = db.Table().Delete(p => p.Test == "Hello World"); + var r = db.Table().Delete (p => p.Test == "Hello World"); Assert.AreEqual (Count, r); Assert.AreEqual (1, db.Table ().Count ()); } + + [Test] + public void DeleteWithWherePredicate () + { + var db = CreateDb (); + + var r = db.Table ().Where (p => p.Test == "Hello World").Delete (); + + Assert.AreEqual (Count, r); + Assert.AreEqual (0, db.Table ().Count ()); + } + + [Test] + public void DeleteWithoutPredicate () + { + var db = CreateDb (); + + try { + var r = db.Table ().Delete (); + Assert.Fail (); + } + catch (InvalidOperationException) { + } + } + + [Test] + public void DeleteWithTake () + { + var db = CreateDb (); + + try { + var r = db.Table ().Where (p => p.Test == "Hello World").Take (2).Delete (); + Assert.Fail (); + } + catch (InvalidOperationException) { + } + } + + [Test] + public void DeleteWithSkip () + { + var db = CreateDb (); + + try { + var r = db.Table ().Where (p => p.Test == "Hello World").Skip (2).Delete (); + Assert.Fail (); + } + catch (InvalidOperationException) { + } + } } } diff --git a/tests/EnumCacheTest.cs b/tests/EnumCacheTest.cs index 74426bc4e..c6163f5dc 100644 --- a/tests/EnumCacheTest.cs +++ b/tests/EnumCacheTest.cs @@ -34,10 +34,42 @@ public enum TestEnumStoreAsInt Value2, + Value3 + } + + public enum TestByteEnumStoreAsInt : byte + { + Value1, + + Value2, + Value3 } - public class TestClassThusNotEnum + public enum TestEnumWithRepeats + { + Value1 = 1, + + Value2 = 2, + + Value2Again = 2, + + Value3 = 3, + } + + [StoreAsText] + public enum TestEnumWithRepeatsAsText + { + Value1 = 1, + + Value2 = 2, + + Value2Again = 2, + + Value3 = 3, + } + + public class TestClassThusNotEnum { } @@ -51,7 +83,7 @@ public void ShouldReturnTrueForEnumStoreAsText() Assert.IsTrue(info.StoreAsText); Assert.IsNotNull(info.EnumValues); - var values = Enum.GetValues(typeof(TestEnumStoreAsText)).Cast().ToList(); + var values = Enum.GetValues(typeof(TestEnumStoreAsText)).Cast().ToList(); for (int i = 0; i < values.Count; i++) { @@ -66,14 +98,16 @@ public void ShouldReturnTrueForEnumStoreAsInt() Assert.IsTrue(info.IsEnum); Assert.IsFalse(info.StoreAsText); - Assert.IsNotNull(info.EnumValues); - - var values = Enum.GetValues(typeof(TestEnumStoreAsInt)).Cast().ToList(); + Assert.IsNull(info.EnumValues); + } + + [Test] + public void ShouldReturnTrueForByteEnumStoreAsInt() + { + var info = EnumCache.GetInfo(); - for (int i = 0; i < values.Count; i++) - { - Assert.AreEqual(values[i].ToString(), info.EnumValues[i]); - } + Assert.IsTrue(info.IsEnum); + Assert.IsFalse(info.StoreAsText); } [Test] @@ -85,5 +119,25 @@ public void ShouldReturnFalseForClass() Assert.IsFalse(info.StoreAsText); Assert.IsNull(info.EnumValues); } - } + + [Test] + public void Issue598_EnumsWithRepeatedValues () + { + var info = EnumCache.GetInfo (); + + Assert.IsTrue (info.IsEnum); + Assert.IsFalse (info.StoreAsText); + Assert.IsNull (info.EnumValues); + } + + [Test] + public void Issue598_EnumsWithRepeatedValuesAsText () + { + var info = EnumCache.GetInfo (); + + Assert.IsTrue (info.IsEnum); + Assert.IsTrue (info.StoreAsText); + Assert.IsNotNull (info.EnumValues); + } + } } diff --git a/tests/EnumNullableTest.cs b/tests/EnumNullableTest.cs new file mode 100644 index 000000000..bc8961209 --- /dev/null +++ b/tests/EnumNullableTest.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +#if NETFX_CORE +using Microsoft.VisualStudio.TestPlatform.UnitTestFramework; +using SetUp = Microsoft.VisualStudio.TestPlatform.UnitTestFramework.TestInitializeAttribute; +using TestFixture = Microsoft.VisualStudio.TestPlatform.UnitTestFramework.TestClassAttribute; +using Test = Microsoft.VisualStudio.TestPlatform.UnitTestFramework.TestMethodAttribute; +#else +using NUnit.Framework; +#endif + +namespace SQLite.Tests +{ + [TestFixture] + public class EnumNullableTests + { + public enum TestEnum + { + Value1, + + Value2, + + Value3 + } + + public class TestObj + { + [PrimaryKey] + public int Id { get; set; } + public TestEnum? Value { get; set; } + + public override string ToString() + { + return string.Format("[TestObj: Id={0}, Value={1}]", Id, Value); + } + + } + + public class TestDb : SQLiteConnection + { + public TestDb(String path) + : base(path) + { + CreateTable(); + } + } + + [Test] + public void ShouldPersistAndReadEnum() + { + var db = new TestDb(TestPath.GetTempFileName()); + + var obj1 = new TestObj() { Id = 1, Value = TestEnum.Value2 }; + var obj2 = new TestObj() { Id = 2, Value = TestEnum.Value3 }; + + var numIn1 = db.Insert(obj1); + var numIn2 = db.Insert(obj2); + Assert.AreEqual(1, numIn1); + Assert.AreEqual(1, numIn2); + + var result = db.Query("select * from TestObj").ToList(); + Assert.AreEqual(2, result.Count); + Assert.AreEqual(obj1.Value, result[0].Value); + Assert.AreEqual(obj2.Value, result[1].Value); + + Assert.AreEqual(obj1.Id, result[0].Id); + Assert.AreEqual(obj2.Id, result[1].Id); + + db.Close(); + } + } +} \ No newline at end of file diff --git a/tests/EnumTest.cs b/tests/EnumTest.cs index 264a82a65..b5d1402f0 100644 --- a/tests/EnumTest.cs +++ b/tests/EnumTest.cs @@ -27,7 +27,17 @@ public enum TestEnum Value3 } - public class TestObj + [StoreAsText] + public enum StringTestEnum + { + Value1, + + Value2, + + Value3 + } + + public class TestObj { [PrimaryKey] public int Id { get; set; } @@ -40,12 +50,27 @@ public override string ToString() } + public class StringTestObj + { + [PrimaryKey] + public int Id { get; set; } + public StringTestEnum Value { get; set; } + + public override string ToString () + { + return string.Format("[StringTestObj: Id={0}, Value={1}]", Id, Value); + } + + } + public class TestDb : SQLiteConnection { public TestDb(String path) : base(path) { CreateTable(); + CreateTable(); + CreateTable (); } } @@ -72,5 +97,74 @@ public void ShouldPersistAndReadEnum() db.Close(); } - } -} \ No newline at end of file + + [Test] + public void ShouldPersistAndReadStringEnum () + { + var db = new TestDb(TestPath.GetTempFileName()); + + var obj1 = new StringTestObj() { Id = 1, Value = StringTestEnum.Value2 }; + var obj2 = new StringTestObj() { Id = 2, Value = StringTestEnum.Value3 }; + + var numIn1 = db.Insert(obj1); + var numIn2 = db.Insert(obj2); + Assert.AreEqual(1, numIn1); + Assert.AreEqual(1, numIn2); + + var result = db.Query("select * from StringTestObj").ToList(); + Assert.AreEqual(2, result.Count); + Assert.AreEqual(obj1.Value, result[0].Value); + Assert.AreEqual(obj2.Value, result[1].Value); + + Assert.AreEqual(obj1.Id, result[0].Id); + Assert.AreEqual(obj2.Id, result[1].Id); + + db.Close(); + } + + public enum ByteTestEnum : byte + { + Value1 = 1, + + Value2 = 2, + + Value3 = 3 + } + + public class ByteTestObj + { + [PrimaryKey] + public int Id { get; set; } + public ByteTestEnum Value { get; set; } + + public override string ToString () + { + return string.Format ("[ByteTestObj: Id={0}, Value={1}]", Id, Value); + } + } + + [Test] + public void Issue33_ShouldPersistAndReadByteEnum () + { + var db = new TestDb (TestPath.GetTempFileName ()); + + var obj1 = new ByteTestObj () { Id = 1, Value = ByteTestEnum.Value2 }; + var obj2 = new ByteTestObj () { Id = 2, Value = ByteTestEnum.Value3 }; + + var numIn1 = db.Insert (obj1); + var numIn2 = db.Insert (obj2); + Assert.AreEqual (1, numIn1); + Assert.AreEqual (1, numIn2); + + var result = db.Query ("select * from ByteTestObj order by Id").ToList (); + Assert.AreEqual (2, result.Count); + Assert.AreEqual (obj1.Value, result[0].Value); + Assert.AreEqual (obj2.Value, result[1].Value); + + Assert.AreEqual (obj1.Id, result[0].Id); + Assert.AreEqual (obj2.Id, result[1].Id); + + db.Close (); + } + } +} diff --git a/tests/IgnoreTest.cs b/tests/IgnoreTest.cs index ec0614af3..596b15c43 100644 --- a/tests/IgnoreTest.cs +++ b/tests/IgnoreTest.cs @@ -91,5 +91,107 @@ public void GetDoesntHaveIgnores () Assert.AreEqual ("Hello", oo.Text); Assert.AreEqual (null, oo.IgnoredText); } + + public class BaseClass + { + [Ignore] + public string ToIgnore { + get; + set; + } + } + + public class TableClass : BaseClass + { + public string Name { get; set; } + } + + [Test] + public void BaseIgnores () + { + var db = new TestDb (); + db.CreateTable (); + + var o = new TableClass { + ToIgnore = "Hello", + Name = "World", + }; + + db.Insert (o); + + var oo = db.Table ().First (); + + Assert.AreEqual (null, oo.ToIgnore); + Assert.AreEqual ("World", oo.Name); + } + + public class RedefinedBaseClass + { + public string Name { get; set; } + public List Values { get; set; } + } + + public class RedefinedClass : RedefinedBaseClass + { + [Ignore] + public new List Values { get; set; } + public string Value { get; set; } + } + + [Test] + public void RedefinedIgnores () + { + var db = new TestDb (); + db.CreateTable (); + + var o = new RedefinedClass { + Name = "Foo", + Value = "Bar", + Values = new List { "hello", "world" }, + }; + + db.Insert (o); + + var oo = db.Table ().First (); + + Assert.AreEqual ("Foo", oo.Name); + Assert.AreEqual ("Bar", oo.Value); + Assert.AreEqual (null, oo.Values); + } + + [AttributeUsage (AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + class DerivedIgnoreAttribute : IgnoreAttribute + { + } + + class DerivedIgnoreClass + { + [PrimaryKey, AutoIncrement] + public int Id { get; set; } + + public string NotIgnored { get; set; } + + [DerivedIgnore] + public string Ignored { get; set; } + } + + [Test] + public void DerivedIgnore () + { + var db = new TestDb (); + db.CreateTable (); + + var o = new DerivedIgnoreClass { + Ignored = "Hello", + NotIgnored = "World", + }; + + db.Insert (o); + + var oo = db.Table ().First (); + + Assert.AreEqual (null, oo.Ignored); + Assert.AreEqual ("World", oo.NotIgnored); + } } } diff --git a/tests/InsertTest.cs b/tests/InsertTest.cs index 144f8a44b..57dcd7a23 100644 --- a/tests/InsertTest.cs +++ b/tests/InsertTest.cs @@ -119,6 +119,26 @@ public void InsertALot() Assert.AreEqual(numCount, n, "Num counted must = num objects"); } + [Test] + public void InsertTraces () + { + var oldTracer = _db.Tracer; + var oldTrace = _db.Trace; + + var traces = new List (); + _db.Tracer = traces.Add; + _db.Trace = true; + + var obj1 = new TestObj () { Text = "GLaDOS loves tracing!" }; + var numIn1 = _db.Insert (obj1); + + Assert.AreEqual (1, numIn1); + Assert.AreEqual (1, traces.Count); + + _db.Tracer = oldTracer; + _db.Trace = oldTrace; + } + [Test] public void InsertTwoTimes() { diff --git a/tests/JoinTest.cs b/tests/JoinTest.cs index db3f31a53..65c94119b 100644 --- a/tests/JoinTest.cs +++ b/tests/JoinTest.cs @@ -64,29 +64,29 @@ class R } //[Test] - public void JoinThenWhere () - { - var q = from ol in _db.Table () - join o in _db.Table () on ol.OrderId equals o.Id - where o.Id == 1 - select new { o.Id, ol.ProductId, ol.Quantity }; + //public void JoinThenWhere () + //{ + // var q = from ol in _db.Table () + // join o in _db.Table () on ol.OrderId equals o.Id + // where o.Id == 1 + // select new { o.Id, ol.ProductId, ol.Quantity }; - var r = System.Linq.Enumerable.ToList (q); + // var r = System.Linq.Enumerable.ToList (q); - Assert.AreEqual (2, r.Count); - } + // Assert.AreEqual (2, r.Count); + //} //[Test] - public void WhereThenJoin () - { - var q = from ol in _db.Table () - where ol.OrderId == 1 - join o in _db.Table () on ol.OrderId equals o.Id - select new { o.Id, ol.ProductId, ol.Quantity }; + //public void WhereThenJoin () + //{ + // var q = from ol in _db.Table () + // where ol.OrderId == 1 + // join o in _db.Table () on ol.OrderId equals o.Id + // select new { o.Id, ol.ProductId, ol.Quantity }; - var r = System.Linq.Enumerable.ToList (q); + // var r = System.Linq.Enumerable.ToList (q); - Assert.AreEqual (2, r.Count); - } + // Assert.AreEqual (2, r.Count); + //} } } diff --git a/tests/LinqTest.cs b/tests/LinqTest.cs index 2ca2f443c..73e2d773d 100644 --- a/tests/LinqTest.cs +++ b/tests/LinqTest.cs @@ -233,5 +233,88 @@ public void Issue303_WhereNot_B() Assert.AreEqual(4, r[1].Id); } } + + [Test] + public void QuerySelectAverage () + { + var db = CreateDb (); + + db.Insert (new Product { + Name = "A", + Price = 20, + TotalSales = 100, + }); + + db.Insert (new Product { + Name = "B", + Price = 10, + TotalSales = 100, + }); + + db.Insert (new Product { + Name = "C", + Price = 1000, + TotalSales = 1, + }); + + var r = db.Table ().Where (x => x.TotalSales > 50).Select (s => s.Price).Average (); + + Assert.AreEqual (15m, r); + } + + interface IEntity + { + int Id { get; set; } + string Value { get; set; } + } + + class Entity : IEntity + { + [AutoIncrement, PrimaryKey] + public int Id { get; set; } + public string Value { get; set; } + } + + static T GetEntity (TestDb db, int id) where T : IEntity, new () + { + return db.Table ().FirstOrDefault (x => x.Id == id); + } + + [Test] + public void CastedParameters () + { + var db = CreateDb (); + db.CreateTable (); + + db.Insert (new Entity { + Value = "Foo", + }); + + var r = GetEntity (db, 1); + + Assert.AreEqual ("Foo", r.Value); + } + + [Test] + public void Issue460_ReplaceWith2Args () + { + var db = CreateDb (); + db.Trace = true; + //db.Tracer = Console.WriteLine; + + db.Insert (new Product { + Name = "I am not B X B", + }); + db.Insert (new Product { + Name = "I am B O B", + }); + + var cl = (from c in db.Table () + where c.Name.Replace (" ", "").Contains ("BOB") + select c).FirstOrDefault (); + + Assert.AreEqual (2, cl.Id); + Assert.AreEqual ("I am B O B", cl.Name); + } } } diff --git a/tests/MappingTest.cs b/tests/MappingTest.cs index df5d0272f..d55f596cf 100644 --- a/tests/MappingTest.cs +++ b/tests/MappingTest.cs @@ -30,24 +30,65 @@ class AFunnyTableName public void HasGoodNames () { var db = new TestDb (); - + db.CreateTable (); var mapping = db.GetMapping (); Assert.AreEqual ("AGoodTableName", mapping.TableName); - Assert.AreEqual ("Id", mapping.Columns [0].Name); - Assert.AreEqual ("AGoodColumnName", mapping.Columns [1].Name); + Assert.AreEqual ("Id", mapping.Columns[0].Name); + Assert.AreEqual ("AGoodColumnName", mapping.Columns[1].Name); + } + + class OverrideNamesBase + { + [PrimaryKey, AutoIncrement] + public int Id { get; set; } + + public virtual string Name { get; set; } + public virtual string Value { get; set; } + } + + class OverrideNamesClass : OverrideNamesBase + { + [Column ("n")] + public override string Name { get; set; } + [Column ("v")] + public override string Value { get; set; } + } + + [Test] + public void OverrideNames () + { + var db = new TestDb (); + db.CreateTable (); + + var cols = db.GetTableInfo ("OverrideNamesClass"); + Assert.AreEqual (3, cols.Count); + Assert.IsTrue (cols.Exists (x => x.Name == "n")); + Assert.IsTrue (cols.Exists (x => x.Name == "v")); + + var o = new OverrideNamesClass { + Name = "Foo", + Value = "Bar", + }; + + db.Insert (o); + + var oo = db.Table ().First (); + + Assert.AreEqual ("Foo", oo.Name); + Assert.AreEqual ("Bar", oo.Value); } #region Issue #86 - [Table("foo")] + [Table ("foo")] public class Foo { - [Column("baz")] - public int Bar { get; set; } + [Column ("baz")] + public int Bar { get; set; } } [Test] @@ -56,16 +97,45 @@ public void Issue86 () var db = new TestDb (); db.CreateTable (); - db.Insert (new Foo { Bar = 42 } ); - db.Insert (new Foo { Bar = 69 } ); + db.Insert (new Foo { Bar = 42 }); + db.Insert (new Foo { Bar = 69 }); - var found42 = db.Table ().Where (f => f.Bar == 42).FirstOrDefault(); + var found42 = db.Table ().Where (f => f.Bar == 42).FirstOrDefault (); Assert.IsNotNull (found42); - var ordered = new List(db.Table().OrderByDescending(f => f.Bar)); - Assert.AreEqual(2, ordered.Count); - Assert.AreEqual(69, ordered[0].Bar); - Assert.AreEqual(42, ordered[1].Bar); + var ordered = new List (db.Table ().OrderByDescending (f => f.Bar)); + Assert.AreEqual (2, ordered.Count); + Assert.AreEqual (69, ordered[0].Bar); + Assert.AreEqual (42, ordered[1].Bar); + } + + #endregion + + #region Issue #572 + + public class OnlyKeyModel + { + [PrimaryKey] + public string MyModelId { get; set; } + } + + [Test] + public void OnlyKey () + { + var db = new TestDb (); + db.CreateTable (); + + db.InsertOrReplace (new OnlyKeyModel { MyModelId = "Foo" }); + var foo = db.Get ("Foo"); + Assert.AreEqual (foo.MyModelId, "Foo"); + + db.Insert (new OnlyKeyModel { MyModelId = "Bar" }); + var bar = db.Get ("Bar"); + Assert.AreEqual (bar.MyModelId, "Bar"); + + db.Update (new OnlyKeyModel { MyModelId = "Foo" }); + var foo2 = db.Get ("Foo"); + Assert.AreEqual (foo2.MyModelId, "Foo"); } #endregion diff --git a/tests/MigrationTest.cs b/tests/MigrationTest.cs index 5d0056efa..fe5a62733 100644 --- a/tests/MigrationTest.cs +++ b/tests/MigrationTest.cs @@ -22,6 +22,7 @@ public class MigrationTest class LowerId { public int Id { get; set; } } + [Table ("Test")] class UpperId { public int ID { get; set; } @@ -39,5 +40,82 @@ public void UpperAndLowerColumnNames () Assert.AreEqual ("Id", cols[0].Name); } } + + [Table ("TestAdd")] + class TestAddBefore + { + [PrimaryKey, AutoIncrement] + public int Id { get; set; } + + public string Name { get; set; } + } + + [Table ("TestAdd")] + class TestAddAfter + { + [PrimaryKey, AutoIncrement] + public int Id { get; set; } + + public string Name { get; set; } + + public int IntValue { get; set; } + public string StringValue { get; set; } + } + + [Test] + public void AddColumns () + { + // + // Init the DB + // + var path = ""; + using (var db = new TestDb (true) { Trace = true }) { + path = db.DatabasePath; + + db.CreateTable (); + + var cols = db.GetTableInfo ("TestAdd"); + Assert.AreEqual (2, cols.Count); + + var o = new TestAddBefore { + Name = "Foo", + }; + + db.Insert (o); + + var oo = db.Table ().First (); + + Assert.AreEqual ("Foo", oo.Name); + } + + // + // Migrate and use it + // + using (var db = new SQLiteConnection (path, true) { Trace = true }) { + + db.CreateTable (); + + var cols = db.GetTableInfo ("TestAdd"); + Assert.AreEqual (4, cols.Count); + + var oo = db.Table ().First (); + + Assert.AreEqual ("Foo", oo.Name); + Assert.AreEqual (0, oo.IntValue); + Assert.AreEqual (null, oo.StringValue); + + var o = new TestAddAfter { + Name = "Bar", + IntValue = 42, + StringValue = "Hello", + }; + db.Insert (o); + + var ooo = db.Get (o.Id); + Assert.AreEqual ("Bar", ooo.Name); + Assert.AreEqual (42, ooo.IntValue); + Assert.AreEqual ("Hello", ooo.StringValue); + } + } } } diff --git a/tests/NotNullAttributeTest.cs b/tests/NotNullAttributeTest.cs index 1028951af..e3cd13bb7 100644 --- a/tests/NotNullAttributeTest.cs +++ b/tests/NotNullAttributeTest.cs @@ -285,7 +285,6 @@ public void UpdateQueryWithNullThrowsException () public void ExecuteNonQueryWithNullThrowsException () { using (TestDb db = new TestDb ()) { - TableMapping map; db.CreateTable (); @@ -297,8 +296,11 @@ public void ExecuteNonQueryWithNullThrowsException () }; db.Insert (obj); - map = db.GetMapping (); - map.GetInsertCommand (db, "OR REPLACE").ExecuteNonQuery (new object[] { 1, null, 123, null, null, null }); + NotNullNoPK obj2 = new NotNullNoPK () { + objectId = 1, + OptionalIntProp = 123, + }; + db.InsertOrReplace (obj2); } catch (NotNullConstraintViolationException) { return; diff --git a/tests/NullableTest.cs b/tests/NullableTest.cs index 29e1e2575..98067d6d0 100644 --- a/tests/NullableTest.cs +++ b/tests/NullableTest.cs @@ -243,5 +243,73 @@ public void StringWhereNotNull() Assert.AreEqual(withEmpty, results[0]); Assert.AreEqual(withData, results[1]); } + + public enum TestIntEnum + { + One = 1, + Two = 2, + } + + [StoreAsText] + public enum TestTextEnum + { + Alpha, + Beta, + } + + public class NullableEnumClass + { + [PrimaryKey, AutoIncrement] + public int ID { get; set; } + + public TestIntEnum? NullableIntEnum { get; set; } + public TestTextEnum? NullableTextEnum { get; set; } + + public override bool Equals (object obj) + { + var other = (NullableEnumClass)obj; + return this.ID == other.ID && this.NullableIntEnum == other.NullableIntEnum && this.NullableTextEnum == other.NullableTextEnum; + } + + public override int GetHashCode () + { + return ID.GetHashCode () + NullableIntEnum.GetHashCode () + NullableTextEnum.GetHashCode (); + } + + public override string ToString () + { + return string.Format ("[NullableEnumClass: ID={0}, NullableIntEnum={1}, NullableTextEnum={2}]", ID, NullableIntEnum, NullableTextEnum); + } + } + + [Test] + [Description ("Create a table with a nullable enum column then insert and select against it")] + public void NullableEnum () + { + SQLiteConnection db = new SQLiteConnection (TestPath.GetTempFileName ()); + db.CreateTable (); + + var withNull = new NullableEnumClass { NullableIntEnum = null, NullableTextEnum = null }; + var with1 = new NullableEnumClass { NullableIntEnum = TestIntEnum.One, NullableTextEnum = null }; + var with2 = new NullableEnumClass { NullableIntEnum = TestIntEnum.Two, NullableTextEnum = null }; + var withNullA = new NullableEnumClass { NullableIntEnum = null, NullableTextEnum = TestTextEnum.Alpha }; + var with1B = new NullableEnumClass { NullableIntEnum = TestIntEnum.One, NullableTextEnum = TestTextEnum.Beta }; + + db.Insert (withNull); + db.Insert (with1); + db.Insert (with2); + db.Insert (withNullA); + db.Insert (with1B); + + var results = db.Table ().OrderBy (x => x.ID).ToArray (); + + Assert.AreEqual (5, results.Length); + + Assert.AreEqual (withNull, results[0]); + Assert.AreEqual (with1, results[1]); + Assert.AreEqual (with2, results[2]); + Assert.AreEqual (withNullA, results[3]); + Assert.AreEqual (with1B, results[4]); + } } } diff --git a/tests/OpenTests.cs b/tests/OpenTests.cs index fc6e06641..435b5bf26 100644 --- a/tests/OpenTests.cs +++ b/tests/OpenTests.cs @@ -42,5 +42,18 @@ public void UnicodePathsAsync() Assert.IsTrue (new FileInfo (path).Length > 0, path); } + + [Test] + public void OpenTemporaryOnDisk() + { + var path = string.Empty; + + Assert.DoesNotThrow (() => { + using (var db = new SQLiteConnection (path, true)) + { + db.CreateTable (); + } + }); + } } } diff --git a/tests/SQLCipherTest.cs b/tests/SQLCipherTest.cs new file mode 100644 index 000000000..9dccb9f86 --- /dev/null +++ b/tests/SQLCipherTest.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +#if NETFX_CORE +using Microsoft.VisualStudio.TestPlatform.UnitTestFramework; +using SetUp = Microsoft.VisualStudio.TestPlatform.UnitTestFramework.TestInitializeAttribute; +using TestFixture = Microsoft.VisualStudio.TestPlatform.UnitTestFramework.TestClassAttribute; +using Test = Microsoft.VisualStudio.TestPlatform.UnitTestFramework.TestMethodAttribute; +#else +using NUnit.Framework; +#endif + +namespace SQLite.Tests +{ + [TestFixture] + public class SQLCipherTest + { + class TestTable + { + [PrimaryKey, AutoIncrement] + public int Id { get; set; } + + public string Value { get; set; } + } + + [Test] + public void SetStringKey () + { + string path; + + var key = "SecretPassword"; + + using (var db = new TestDb ()) { + path = db.DatabasePath; + + db.SetKey (key); + + db.CreateTable (); + db.Insert (new TestTable { Value = "Hello" }); + } + + using (var db = new TestDb (path)) { + path = db.DatabasePath; + + db.SetKey (key); + + var r = db.Table ().First (); + + Assert.AreEqual ("Hello", r.Value); + } + } + + [Test] + public void SetBytesKey () + { + string path; + + var rand = new Random (); + var key = new byte[32]; + rand.NextBytes (key); + + using (var db = new TestDb ()) { + path = db.DatabasePath; + + db.SetKey (key); + + db.CreateTable (); + db.Insert (new TestTable { Value = "Hello" }); + } + + using (var db = new TestDb (path)) { + path = db.DatabasePath; + + db.SetKey (key); + + var r = db.Table ().First (); + + Assert.AreEqual ("Hello", r.Value); + } + } + + [Test] + public void SetBadBytesKey () + { + try { + using (var db = new TestDb ()) { + db.SetKey (new byte[] { 1, 2, 3, 4 }); + } + Assert.Fail ("Should have thrown"); + } + catch (ArgumentException) { + } + } + } +} diff --git a/tests/SQLite.Tests.csproj b/tests/SQLite.Tests.csproj index 543568fbc..674f1cbe2 100644 --- a/tests/SQLite.Tests.csproj +++ b/tests/SQLite.Tests.csproj @@ -43,7 +43,9 @@ + + @@ -78,6 +80,7 @@ + diff --git a/tests/SQLite.Tests.iOS/SQLite.Tests.iOS.csproj b/tests/SQLite.Tests.iOS/SQLiteTestsiOS.csproj similarity index 64% rename from tests/SQLite.Tests.iOS/SQLite.Tests.iOS.csproj rename to tests/SQLite.Tests.iOS/SQLiteTestsiOS.csproj index 26748dec2..4acd4ebd6 100644 --- a/tests/SQLite.Tests.iOS/SQLite.Tests.iOS.csproj +++ b/tests/SQLite.Tests.iOS/SQLiteTestsiOS.csproj @@ -1,4 +1,4 @@ - + Debug @@ -9,7 +9,7 @@ {FEACFBD2-3405-455C-9665-78FE426C6842};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} Exe SQLite.Tests.iOS - SQLite.Tests.iOS + SQLiteTestsiOS Resources @@ -24,43 +24,41 @@ true true true - true - true None - i386 + x86_64 HttpClientHandler Default false + true - + + true bin\iPhone\Release __UNIFIED__;__MOBILE__;__IOS__;NO_VB prompt 4 iPhone Developer - true - true true - + + SdkOnly ARMv7, ARM64 HttpClientHandler Default - + + true bin\iPhoneSimulator\Release __UNIFIED__;__MOBILE__;__IOS__;NO_VB prompt 4 iPhone Developer - true - true None - i386 + x86_64 HttpClientHandler Default @@ -73,18 +71,18 @@ prompt 4 iPhone Developer - true + false true - true + false true - true - true true - + + SdkOnly - ARMv7, ARM64 + ARM64 HttpClientHandler Default + true @@ -103,7 +101,43 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ConcurrencyTest.cs + SQLite.cs diff --git a/tests/ScalarTest.cs b/tests/ScalarTest.cs index 339d073cd..757024f02 100644 --- a/tests/ScalarTest.cs +++ b/tests/ScalarTest.cs @@ -45,6 +45,16 @@ public void Int32 () Assert.AreEqual (Count * 2, r); } + + [Test] + public void SelectSingleRowValue () + { + var db = CreateDb (); + + var r = db.ExecuteScalar ("SELECT Two FROM TestTable WHERE Id = 1 LIMIT 1"); + + Assert.AreEqual (2, r); + } } } diff --git a/tests/StringQueryTest.cs b/tests/StringQueryTest.cs index 35976666f..ae474b91c 100644 --- a/tests/StringQueryTest.cs +++ b/tests/StringQueryTest.cs @@ -62,8 +62,16 @@ public void StartsWith () { var fs = db.Table ().Where (x => x.Name.StartsWith ("F")).ToList (); Assert.AreEqual (2, fs.Count); - - var bs = db.Table ().Where (x => x.Name.StartsWith ("B")).ToList (); + + var lfs = db.Table().Where(x => x.Name.StartsWith("f")).ToList(); + Assert.AreEqual(0, lfs.Count); + + + var lfs2 = db.Table().Where(x => x.Name.StartsWith("f",StringComparison.OrdinalIgnoreCase)).ToList(); + Assert.AreEqual(2, lfs2.Count); + + + var bs = db.Table ().Where (x => x.Name.StartsWith ("B")).ToList (); Assert.AreEqual (1, bs.Count); } @@ -71,20 +79,32 @@ public void StartsWith () public void EndsWith () { var fs = db.Table ().Where (x => x.Name.EndsWith ("ar")).ToList (); - Assert.AreEqual (2, fs.Count); - - var bs = db.Table ().Where (x => x.Name.EndsWith ("o")).ToList (); + Assert.AreEqual (2, fs.Count); + + var lfs = db.Table().Where(x => x.Name.EndsWith("Ar")).ToList(); + Assert.AreEqual(0, lfs.Count); + + var bs = db.Table ().Where (x => x.Name.EndsWith ("o")).ToList (); Assert.AreEqual (1, bs.Count); } [Test] public void Contains () { - var fs = db.Table ().Where (x => x.Name.Contains ("o")).ToList (); - Assert.AreEqual (2, fs.Count); - - var bs = db.Table ().Where (x => x.Name.Contains ("a")).ToList (); + var fs = db.Table().Where(x => x.Name.Contains("o")).ToList(); + Assert.AreEqual(2, fs.Count); + + var lfs = db.Table ().Where (x => x.Name.Contains ("O")).ToList (); + Assert.AreEqual (0, lfs.Count); + + var lfsu = db.Table().Where(x => x.Name.ToUpper().Contains("O")).ToList(); + Assert.AreEqual(2, lfsu.Count); + + var bs = db.Table ().Where (x => x.Name.Contains ("a")).ToList (); Assert.AreEqual (2, bs.Count); - } + + var zs = db.Table().Where(x => x.Name.Contains("z")).ToList(); + Assert.AreEqual(0, zs.Count); + } } } diff --git a/tests/TestDb.cs b/tests/TestDb.cs index 50d1c32da..3891cf62c 100644 --- a/tests/TestDb.cs +++ b/tests/TestDb.cs @@ -57,6 +57,11 @@ public TestDb (bool storeDateTimeAsTicks = true) : base (TestPath.GetTempFileNam { Trace = true; } + + public TestDb (string path, bool storeDateTimeAsTicks = true) : base (path, storeDateTimeAsTicks) + { + Trace = true; + } } public class TestPath diff --git a/tests/TransactionTest.cs b/tests/TransactionTest.cs index 77bd48cb0..469598b9b 100644 --- a/tests/TransactionTest.cs +++ b/tests/TransactionTest.cs @@ -121,6 +121,161 @@ public void FailNestedSavepointTransaction() Assert.AreEqual(testObjects.Count, db.Table().Count()); } + + [Test] + public void Issue329_AsyncTransactionFailuresShouldRollback () + { + var adb = new SQLiteAsyncConnection (TestPath.GetTempFileName ()); + adb.CreateTableAsync ().Wait (); + var initialCount = adb.Table ().CountAsync ().Result; + var rollbacks = 0; + + // + // Fail a commit + // + adb.Trace = true; + adb.Tracer = m => { + Console.WriteLine (m); + if (m == "Executing: rollback") + rollbacks++; + }; + + try { + adb.RunInTransactionAsync (db => { + db.Insert (new TestObj ()); + throw new Exception ("User exception"); + }).Wait (); + Assert.Fail ("Should have thrown"); + } + catch (AggregateException aex) + when (aex.InnerException.Message == "User exception") { + // Expected + } + + Assert.AreEqual (1, rollbacks); + } + + [Test] + public void Issue604_RunInTransactionAsync () + { + var adb = new SQLiteAsyncConnection (TestPath.GetTempFileName ()); + adb.CreateTableAsync ().Wait (); + var initialCount = adb.Table ().CountAsync ().Result; + + // + // Fail a commit + // + adb.Trace = true; + adb.Tracer = m => { + //Console.WriteLine (m); + if (m.Trim().EndsWith ("commit")) + throw SQLiteException.New (SQLite3.Result.Busy, "Make commit fail"); + }; + + try { + adb.RunInTransactionAsync (db => { + db.Insert (new TestObj ()); + }).Wait (); + Assert.Fail ("Should have thrown"); + } + catch (AggregateException aex) + when (aex.InnerException is SQLiteException ex + && ex.Result == SQLite3.Result.Busy) { + // Expected + } + + // + // Are we stuck? + // + adb.Tracer = null; + adb.RunInTransactionAsync (db => { + db.Insert (new TestObj ()); + }).Wait (); + + Assert.AreEqual (initialCount + 1, adb.Table ().CountAsync ().Result); + } + + [Test] + public void Issue604_RecoversFromFailedCommit () + { + db.Trace = true; + var initialCount = db.Table ().Count (); + + // + // Well this is an issue because there is an internal variable called _transactionDepth + // that tries to track if we are in an active transaction. + // The problem is, _transactionDepth is set to 0 and then commit is executed on the database. + // Well, the commit fails and "When COMMIT fails in this way, the transaction remains active and + // the COMMIT can be retried later after the reader has had a chance to clear" + // + var rollbacks = 0; + db.Tracer = m => { + if (m == "Executing: commit") + throw SQLiteException.New (SQLite3.Result.Busy, "Make commit fail"); + if (m == "Executing: rollback") + rollbacks++; + }; + db.BeginTransaction (); + db.Insert (new TestObj ()); + try { + db.Commit (); + Assert.Fail ("Should have thrown"); + } + catch (SQLiteException ex) when (ex.Result == SQLite3.Result.Busy) { + db.Tracer = null; + } + Assert.False (db.IsInTransaction); + Assert.AreEqual (1, rollbacks); + + // + // The catch statements in the RunInTransaction family of functions catch this and call rollback, + // but since _transactionDepth is 0, the transaction isn't actually rolled back. + // + // So the next time begin transaction is called on the same connection, + // sqlite-net attempts to begin a new transaction (because _transactionDepth is 0), + // which promptly fails because there is still an active transaction on the connection. + // + // Well now we are in big trouble because _transactionDepth got set to 1, + // and when begin transaction fails in this manner, the transaction isn't rolled back + // (which would have set _transactionDepth to 0) + // + db.BeginTransaction (); + db.Insert (new TestObj ()); + db.Commit (); + Assert.AreEqual (initialCount + 1, db.Table ().Count ()); + } + + [Test] + public void Issue604_RecoversFromFailedRelease () + { + db.Trace = true; + var initialCount = db.Table ().Count (); + + var rollbacks = 0; + db.Tracer = m => { + //Console.WriteLine (m); + if (m.StartsWith ("Executing: release")) + throw SQLiteException.New (SQLite3.Result.Busy, "Make release fail"); + if (m == "Executing: rollback") + rollbacks++; + }; + var sp0 = db.SaveTransactionPoint (); + db.Insert (new TestObj ()); + try { + db.Release (sp0); + Assert.Fail ("Should have thrown"); + } + catch (SQLiteException ex) when (ex.Result == SQLite3.Result.Busy) { + db.Tracer = null; + } + Assert.False (db.IsInTransaction); + Assert.AreEqual (1, rollbacks); + + db.BeginTransaction (); + db.Insert (new TestObj ()); + db.Commit (); + Assert.AreEqual (initialCount + 1, db.Table ().Count ()); + } } }