Index: Setup/data/verify.lst ================================================================== --- Setup/data/verify.lst +++ Setup/data/verify.lst @@ -775,10 +775,11 @@ Tests/tkt-3567020edf.eagle Tests/tkt-393d954be0.eagle Tests/tkt-3aa50d8413.eagle Tests/tkt-3c00ec5b52.eagle Tests/tkt-3e783eecbe.eagle + Tests/tkt-41aea496e0.eagle Tests/tkt-448d663d11.eagle Tests/tkt-47c6fa04d3.eagle Tests/tkt-47f4bac575.eagle Tests/tkt-48a6b8e4ca.eagle Tests/tkt-4a791e70ab.eagle Index: System.Data.SQLite.Linq/SQL Generation/DmlSqlGenerator.cs ================================================================== --- System.Data.SQLite.Linq/SQL Generation/DmlSqlGenerator.cs +++ System.Data.SQLite.Linq/SQL Generation/DmlSqlGenerator.cs @@ -81,11 +81,11 @@ commandText.Append("WHERE "); tree.Predicate.Accept(translator); commandText.AppendLine(";"); // generate returning sql - GenerateReturningSql(commandText, tree, translator, tree.Returning); + GenerateReturningSql(commandText, tree, translator, tree.Returning, false); parameters = translator.Parameters; return commandText.ToString(); } @@ -148,11 +148,11 @@ { commandText.AppendLine(" DEFAULT VALUES;"); } // generate returning sql - GenerateReturningSql(commandText, tree, translator, tree.Returning); + GenerateReturningSql(commandText, tree, translator, tree.Returning, true); parameters = translator.Parameters; return commandText.ToString(); } @@ -161,10 +161,91 @@ // SQL gen, where we only access table columns) private static string GenerateMemberTSql(EdmMember member) { return SqlGenerator.QuoteIdentifier(member.Name); } + + /// + /// This method attempts to determine if the specified table has an integer + /// primary key (i.e. "rowid"). If so, it sets the + /// parameter to the right + /// ; otherwise, the + /// parameter is set to null. + /// + /// The table to check. + /// + /// The collection of key members. An attempt is always made to set this + /// parameter to a valid value. + /// + /// + /// The that represents the integer primary key + /// -OR- null if no such exists. + /// + /// + /// Non-zero if the specified table has an integer primary key. + /// + private static bool IsIntegerPrimaryKey( + EntitySetBase table, + out ReadOnlyMetadataCollection keyMembers, + out EdmMember primaryKeyMember + ) + { + keyMembers = table.ElementType.KeyMembers; + + if (keyMembers.Count == 1) /* NOTE: The "rowid" only? */ + { + EdmMember keyMember = keyMembers[0]; + PrimitiveTypeKind typeKind; + + if (MetadataHelpers.TryGetPrimitiveTypeKind( + keyMember.TypeUsage, out typeKind) && + (typeKind == PrimitiveTypeKind.Int64)) + { + primaryKeyMember = keyMember; + return true; + } + } + + primaryKeyMember = null; + return false; + } + + /// + /// This method attempts to determine if all the specified key members have + /// values available. + /// + /// + /// The to use. + /// + /// + /// The collection of key members to check. + /// + /// + /// The first missing key member that is found. This is only set to a valid + /// value if the method is returning false. + /// + /// + /// Non-zero if all key members have values; otherwise, zero. + /// + private static bool DoAllKeyMembersHaveValues( + ExpressionTranslator translator, + ReadOnlyMetadataCollection keyMembers, + out EdmMember missingKeyMember + ) + { + foreach (EdmMember keyMember in keyMembers) + { + if (!translator.MemberValues.ContainsKey(keyMember)) + { + missingKeyMember = keyMember; + return false; + } + } + + missingKeyMember = null; + return true; + } /// /// Generates SQL fragment returning server-generated values. /// Requires: translator knows about member values so that we can figure out /// how to construct the key predicate. @@ -188,12 +269,16 @@ /// Modification command tree /// Translator used to produce DML SQL statement /// for the tree /// Returning expression. If null, the method returns /// immediately without producing a SELECT statement. + /// + /// Non-zero if this method is being called as part of processing an INSERT; + /// otherwise (e.g. UPDATE), zero. + /// private static void GenerateReturningSql(StringBuilder commandText, DbModificationCommandTree tree, - ExpressionTranslator translator, DbExpression returning) + ExpressionTranslator translator, DbExpression returning, bool wasInsert) { // Nothing to do if there is no Returning expression if (null == returning) { return; } // select @@ -210,38 +295,128 @@ #if USE_INTEROP_DLL && INTEROP_EXTENSION_FUNCTIONS commandText.Append("WHERE last_rows_affected() > 0"); #else commandText.Append("WHERE changes() > 0"); #endif + EntitySetBase table = ((DbScanExpression)tree.Target.Expression).Target; - bool identity = false; - foreach (EdmMember keyMember in table.ElementType.KeyMembers) - { - commandText.Append(" AND "); - commandText.Append(GenerateMemberTSql(keyMember)); - commandText.Append(" = "); - - // retrieve member value sql. the translator remembers member values - // as it constructs the DML statement (which precedes the "returning" - // SQL) - DbParameter value; - if (translator.MemberValues.TryGetValue(keyMember, out value)) - { - commandText.Append(value.ParameterName); - } - else - { - // if no value is registered for the key member, it means it is an identity - // which can be retrieved using the scope_identity() function - if (identity) - { - // there can be only one server generated key - throw new NotSupportedException(string.Format("Server generated keys are only supported for identity columns. More than one key column is marked as server generated in table '{0}'.", table.Name)); - } - commandText.AppendLine("last_insert_rowid()"); - identity = true; - } + ReadOnlyMetadataCollection keyMembers; + EdmMember primaryKeyMember; + EdmMember missingKeyMember; + + // Model Types can be (at the time of this implementation): + // Binary, Boolean, Byte, DateTime, Decimal, Double, Guid, Int16, + // Int32, Int64,Single, String + if (IsIntegerPrimaryKey(table, out keyMembers, out primaryKeyMember)) + { + // + // NOTE: This must be an INTEGER PRIMARY KEY (i.e. "rowid") table. + // + commandText.Append(" AND "); + commandText.Append(GenerateMemberTSql(primaryKeyMember)); + commandText.Append(" = "); + + DbParameter value; + + if (translator.MemberValues.TryGetValue(primaryKeyMember, out value)) + { + // + // NOTE: Use the integer primary key value that was specified as + // part the associated INSERT/UPDATE statement. + // + commandText.Append(value.ParameterName); + } + else if (wasInsert) + { + // + // NOTE: This was part of an INSERT statement and we know the table + // has an integer primary key. This should not fail unless + // something (e.g. a trigger) causes the last_insert_rowid() + // function to return an incorrect result. + // + commandText.AppendLine("last_insert_rowid()"); + } + else /* NOT-REACHED? */ + { + // + // NOTE: We cannot simply use the "rowid" at this point because: + // + // 1. The last_insert_rowid() function is only valid after + // an INSERT and this was an UPDATE. + // + throw new NotSupportedException(String.Format( + "Missing value for INSERT key member '{0}' in table '{1}'.", + (primaryKeyMember != null) ? primaryKeyMember.Name : "", + table.Name)); + } + } + else if (DoAllKeyMembersHaveValues(translator, keyMembers, out missingKeyMember)) + { + foreach (EdmMember keyMember in keyMembers) + { + commandText.Append(" AND "); + commandText.Append(GenerateMemberTSql(keyMember)); + commandText.Append(" = "); + + // Retrieve member value SQL. the translator remembers member values + // as it constructs the DML statement (which precedes the "returning" + // SQL). + DbParameter value; + + if (translator.MemberValues.TryGetValue(keyMember, out value)) + { + // + // NOTE: Use the primary key value that was specified as part the + // associated INSERT/UPDATE statement. This also applies + // to composite primary keys. + // + commandText.Append(value.ParameterName); + } + else /* NOT-REACHED? */ + { + // + // NOTE: We cannot simply use the "rowid" at this point because: + // + // 1. This associated INSERT/UPDATE statement appeared to + // have all the key members availab;e however, there + // appears to be an inconsistency. This is an internal + // error and should be thrown. + // + throw new NotSupportedException(String.Format( + "Missing value for {0} key member '{1}' in table '{2}' " + + "(internal).", wasInsert ? "INSERT" : "UPDATE", + (keyMember != null) ? keyMember.Name : "", + table.Name)); + } + } + } + else if (wasInsert) /* NOT-REACHED? */ + { + // + // NOTE: This was part of an INSERT statement; try using the "rowid" + // column to fetch the most recently inserted row. This may + // still fail if the table is a WITHOUT ROWID table -OR- + // something (e.g. a trigger) causes the last_insert_rowid() + // function to return an incorrect result. + // + commandText.Append(" AND "); + commandText.Append(SqlGenerator.QuoteIdentifier("rowid")); + commandText.Append(" = "); + commandText.AppendLine("last_insert_rowid()"); + } + else /* NOT-REACHED? */ + { + // + // NOTE: We cannot simply use the "rowid" at this point because: + // + // 1. The last_insert_rowid() function is only valid after + // an INSERT and this was an UPDATE. + // + throw new NotSupportedException(String.Format( + "Missing value for UPDATE key member '{0}' in table '{1}'.", + (missingKeyMember != null) ? missingKeyMember.Name : "", + table.Name)); } commandText.AppendLine(";"); } /// ADDED Tests/tkt-41aea496e0.eagle Index: Tests/tkt-41aea496e0.eagle ================================================================== --- /dev/null +++ Tests/tkt-41aea496e0.eagle @@ -0,0 +1,65 @@ +############################################################################### +# +# tkt-41aea496e0.eagle -- +# +# Written by Joe Mistachkin. +# Released to the public domain, use at your own risk! +# +############################################################################### + +package require Eagle +package require Eagle.Library +package require Eagle.Test + +runTestPrologue + +############################################################################### + +package require System.Data.SQLite.Test +runSQLiteTestPrologue +runSQLiteTestFilesPrologue + +############################################################################### + +runTest {test tkt-41aea496e0-1.1 {LINQ non-rowid primary key support} -body { + # + # NOTE: Re-copy the reference database file used for this unit test to the + # build directory in case it has been changed by a previous test run. + # + file copy -force $northwindEfDbFile \ + [file join [getBuildDirectory] [file tail $northwindEfDbFile]] + + set result [list] + set output "" + + set code [catch { + testClrExec $testLinqExeFile [list -eventflags Wait -directory \ + [file dirname $testLinqExeFile] -nocarriagereturns -stdout output \ + -success 0] -complexprimarykey + } error] + + tlog "---- BEGIN STDOUT OUTPUT\n" + tlog $output + tlog "\n---- END STDOUT OUTPUT\n" + + lappend result $code + + if {$code == 0} then { + lappend result [string trim $output] + } else { + lappend result [string trim $error] + } + + set result +} -cleanup { + unset -nocomplain code output error result +} -constraints {eagle monoToDo SQLite file_System.Data.SQLite.dll testExec\ +file_System.Data.SQLite.Linq.dll file_testlinq.exe file_northwindEF.db} \ +-result {0 {inserted 2 +updated 2}}} + +############################################################################### + +runSQLiteTestFilesEpilogue +runSQLiteTestEpilogue +runTestEpilogue Index: Tests/tkt-9d353b0bd8.eagle ================================================================== --- Tests/tkt-9d353b0bd8.eagle +++ Tests/tkt-9d353b0bd8.eagle @@ -52,12 +52,12 @@ set result } -cleanup { unset -nocomplain code output error result } -constraints {eagle monoToDo SQLite file_System.Data.SQLite.dll testExec\ -file_System.Data.SQLite.Linq.dll file_testlinq.exe file_northwindEF.db\ -System.Data.SQLite.dll_v4.0.30319} -result {0 {inserted 1}}} +file_System.Data.SQLite.Linq.dll file_testlinq.exe file_northwindEF.db} \ +-result {0 {inserted 1}}} ############################################################################### runSQLiteTestFilesEpilogue runSQLiteTestEpilogue Index: testlinq/Program.cs ================================================================== --- testlinq/Program.cs +++ testlinq/Program.cs @@ -4,10 +4,12 @@ * * Released to the public domain, use at your own risk! ********************************************************/ using System; +using System.Collections.Generic; +using System.Data; using System.Data.Common; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Text; @@ -144,16 +146,14 @@ } } return EFTransactionTest(value); } -#if NET_40 || NET_45 || NET_451 || NET_46 case "insert": { return InsertTest(); } -#endif case "update": { return UpdateTest(); } case "binaryguid": @@ -198,10 +198,14 @@ case "round": { return RoundTest(); } #endif + case "complexprimarykey": + { + return ComplexPrimaryKeyTest(); + } default: { Console.WriteLine("unknown test \"{0}\"", arg); return 1; } @@ -556,20 +560,21 @@ } return 0; } -#if NET_40 || NET_45 || NET_451 || NET_46 // // NOTE: Used to test the INSERT fix (i.e. an extra semi-colon in // the SQL statement after the actual INSERT statement in // the follow-up SELECT statement). // private static int InsertTest() { using (northwindEFEntities db = new northwindEFEntities()) { + long orderId = 10248; + long productId = 1; int[] counts = { 0 }; // // NOTE: *REQUIRED* This is required so that the // Entity Framework is prevented from opening @@ -578,17 +583,33 @@ // IMMEDIATE transactions, thereby failing [later // on] with locking errors). // db.Connection.Open(); + KeyValuePair orderIdPair = + new KeyValuePair("OrderID", orderId); + + KeyValuePair productIdPair = + new KeyValuePair("ProductID", productId); + + ///////////////////////////////////////////////////////////////// + OrderDetails newOrderDetails = new OrderDetails(); - newOrderDetails.OrderID = 10248; - newOrderDetails.ProductID = 1; + newOrderDetails.OrderID = orderId; + newOrderDetails.ProductID = productId; newOrderDetails.UnitPrice = (decimal)1.23; newOrderDetails.Quantity = 1; newOrderDetails.Discount = 0.0f; + + newOrderDetails.OrdersReference.EntityKey = new EntityKey( + "northwindEFEntities.Orders", + new KeyValuePair[] { orderIdPair }); + + newOrderDetails.ProductsReference.EntityKey = new EntityKey( + "northwindEFEntities.Products", + new KeyValuePair[] { productIdPair }); db.AddObject("OrderDetails", newOrderDetails); try { @@ -607,11 +628,10 @@ Console.WriteLine("inserted {0}", counts[0]); } return 0; } -#endif // // NOTE: Used to test the UPDATE fix (i.e. the missing semi-colon // in the SQL statement between the actual UPDATE statement // and the follow-up SELECT statement). @@ -752,10 +772,116 @@ null); return 0; } #endif + + private static int ComplexPrimaryKeyTest() + { + using (northwindEFEntities db = new northwindEFEntities()) + { + long orderId = 10248; + long productId = 1; + int[] counts = { 0, 0 }; + + // + // NOTE: *REQUIRED* This is required so that the + // Entity Framework is prevented from opening + // multiple connections to the underlying SQLite + // database (i.e. which would result in multiple + // IMMEDIATE transactions, thereby failing [later + // on] with locking errors). + // + db.Connection.Open(); + + KeyValuePair orderIdPair = + new KeyValuePair("OrderID", orderId); + + KeyValuePair productIdPair = + new KeyValuePair("ProductID", productId); + + ///////////////////////////////////////////////////////////////// + + OrderDetails newOrderDetails = new OrderDetails(); + + newOrderDetails.OrderID = orderId; + newOrderDetails.ProductID = productId; + newOrderDetails.UnitPrice = (decimal)1.23; + newOrderDetails.Quantity = 1; + newOrderDetails.Discount = 0.0f; + + newOrderDetails.OrdersReference.EntityKey = new EntityKey( + "northwindEFEntities.Orders", + new KeyValuePair[] { orderIdPair }); + + newOrderDetails.ProductsReference.EntityKey = new EntityKey( + "northwindEFEntities.Products", + new KeyValuePair[] { productIdPair }); + + db.AddObject("OrderDetails", newOrderDetails); + + try + { + db.SaveChanges(); + counts[0]++; + } + catch (Exception e) + { + Console.WriteLine(e); + } + finally + { + db.AcceptAllChanges(); + } + + try + { + db.Refresh(RefreshMode.StoreWins, newOrderDetails); + counts[0]++; + } + catch (Exception e) + { + Console.WriteLine(e); + } + + Console.WriteLine("inserted {0}", counts[0]); + + ///////////////////////////////////////////////////////////////// + + newOrderDetails.UnitPrice = (decimal)2.34; + newOrderDetails.Quantity = 2; + newOrderDetails.Discount = 0.1f; + + try + { + db.SaveChanges(); + counts[1]++; + } + catch (Exception e) + { + Console.WriteLine(e); + } + finally + { + db.AcceptAllChanges(); + } + + try + { + db.Refresh(RefreshMode.StoreWins, newOrderDetails); + counts[1]++; + } + catch (Exception e) + { + Console.WriteLine(e); + } + + Console.WriteLine("updated {0}", counts[1]); + } + + return 0; + } private static int DateTimeTest() { using (northwindEFEntities db = new northwindEFEntities()) {