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())
{