/// <summary> /// Hack the query /// </summary> public bool HackQuery(QueryBuilder builder, SqlStatement sqlStatement, SqlStatement whereClause, Type tmodel, PropertyInfo property, string queryPrefix, QueryPredicate predicate, object values, IEnumerable <TableMapping> scopedTables, params KeyValuePair <string, object>[] queryFilter) { if (typeof(SecurityUser) == tmodel && property.Name == nameof(SecurityUser.UserEntity)) { var userkey = TableMapping.Get(typeof(DbUserEntity)).GetColumn(nameof(DbUserEntity.SecurityUserKey), false); var personSubSelect = builder.CreateQuery <UserEntity>(queryFilter.Select(p => new KeyValuePair <String, Object>(p.Key.Replace("userEntity.", ""), p.Value)), null, userkey); var userIdKey = TableMapping.Get(typeof(DbSecurityUser)).PrimaryKey.FirstOrDefault(); whereClause.And($"{userIdKey.Name} IN (").Append(personSubSelect).Append(")"); return(true); } return(false); }
public void TestCreateWithNonAutoIncrementPrimaryKey() { var table = TableMapping.Get <TestObjectWithNonAutoIncrementPrimaryKey>(); var id = table.Columns["Id"]; Assert.Equal(id.ClrType, typeof(long)); // No way to test the PropertyInfo directly Assert.Equal(id.Metadata.CollationSequence, "BINARY"); Assert.Equal(id.Metadata.DeclaredType, "INTEGER"); Assert.True(id.Metadata.HasNotNullConstraint); Assert.False(id.Metadata.IsAutoIncrement); Assert.True(id.Metadata.IsPrimaryKeyPart); }
/// <summary> /// Converts a <see cref="DbMailMessage"/> instance to a <see cref="MailMessage"/> instance. /// </summary> /// <param name="dataInstance">The db alert message instance.</param> /// <param name="context">The data context.</param> /// <param name="principal">The authentication context.</param> /// <returns>Returns the converted instance.</returns> public override MailMessage ToModelInstance(object dataInstance, DataContext context) { var modelInstance = base.ToModelInstance(dataInstance, context); modelInstance.Flags = (MailMessageFlags)(dataInstance as DbMailMessage).Flags; var tableMapping = TableMapping.Get(typeof(DbSecurityUser)); SqlStatement rcptStatement = context.CreateSqlStatement() .SelectFrom(tableMapping.OrmType, tableMapping.Columns.First(o => o.IsPrimaryKey)) .InnerJoin <DbSecurityUser, DbMailMessageRcptTo> (o => o.Key, o => o.SourceKey) .Where <DbMailMessageRcptTo>(o => o.Key == modelInstance.Key).Build(); modelInstance.RcptToXml = context.Query <DbSecurityUser>(rcptStatement).Select(o => o.Key).ToList(); return(modelInstance); }
/// <summary> /// Ensure the effective version sequence is set /// </summary> public override TModel UpdateInternal(DataContext context, TModel data) { if (!data.EffectiveVersionSequenceId.HasValue) { var dbSourceType = this.m_settingsProvider.GetMapper().MapModelType(data.SourceType); var tableMap = TableMapping.Get(dbSourceType); var sql = context.CreateSqlStatement() .SelectFrom(dbSourceType, tableMap.GetColumn(nameof(IDbVersionedData.VersionSequenceId))) .Where($"{tableMap.GetColumn(nameof(IDbVersionedData.Key)).Name} = ?", data.SourceEntityKey) .And($"{tableMap.GetColumn(nameof(IDbVersionedData.ObsoletionTime)).Name} IS NOT NULL") .Append($"ORDER BY {tableMap.GetColumn(nameof(IDbVersionedData.VersionSequenceId))} DESC"); data.EffectiveVersionSequenceId = context.FirstOrDefault <Int32>(sql); } return(base.UpdateInternal(context, data)); }
public void TestInitTable() { var table = TableMapping.Get <TestObject>(); using (var db = SQLite3.OpenInMemory()) { db.InitTable <TestObject>(); var dbIndexes = db.GetIndexInfo(table.TableName); Assert.Equal(table.Indexes, dbIndexes); var dbColumns = db.GetTableInfo(table.TableName); Assert.Equal( table.Columns.Select(x => new KeyValuePair <string, TableColumnMetadata>(x.Key, x.Value.Metadata)), dbColumns); } }
public void TestInitTableWithMigration() { var tableOriginal = TableMapping.Get <TestMutableObject>(); var tableOriginalOrm = Orm.ResultSet.RowToObject <TestMutableObject>(); var tableNew = TableMapping.Get <TestMutableObjectUpdated>(); var tableNewOrm = Orm.ResultSet.RowToObject <TestMutableObjectUpdated>(); using (var db = SQLite3.OpenInMemory()) { db.InitTable <TestMutableObject>(); var dbIndexes = db.GetIndexInfo(tableOriginal.TableName); Assert.Equal(tableOriginal.Indexes, dbIndexes); var dbColumns = db.GetTableInfo(tableOriginal.TableName); Assert.Equal( tableOriginal.Columns.Select(x => new KeyValuePair <string, TableColumnMetadata>(x.Key, x.Value.Metadata)), dbColumns); var insertedOriginal = db.InsertOrReplace(new TestMutableObject() { Value = "test" }, tableOriginalOrm); db.InitTable <TestMutableObjectUpdated>(); dbIndexes = db.GetIndexInfo(tableOriginal.TableName); Assert.Equal(tableNew.Indexes, dbIndexes); dbColumns = db.GetTableInfo(tableOriginal.TableName); Assert.Equal( tableNew.Columns.Select(x => new KeyValuePair <string, TableColumnMetadata>(x.Key, x.Value.Metadata)), dbColumns); TestMutableObjectUpdated found; if (db.TryFind <TestMutableObjectUpdated>(insertedOriginal.Id.Value, tableNewOrm, out found)) { Assert.Equal(found.NotNullReference, "default"); } else { Assert.True(false); } } }
/// <summary> /// Get secure key for the unknown application name /// </summary> public byte[] GetSecureKey(string name) { using (DataContext dataContext = this.m_configuration.Provider.GetWriteConnection()) try { dataContext.Open(); var dbType = TableMapping.Get(typeof(DbSecurityApplication)); var stmt = dataContext.CreateSqlStatement().SelectFrom(dbType.OrmType, dbType.Columns.First(o => o.SourceProperty.Name == nameof(DbSecurityApplication.Secret))) .Where <DbSecurityApplication>(o => o.PublicId == name); var secret = dataContext.FirstOrDefault <String>(stmt.Build()); // Secret is the key return(secret.ParseHexString()); } catch (Exception e) { this.m_traceSource.TraceEvent(EventLevel.Error, "Error setting secret for {0} : {1}", name, e); throw new DataPersistenceException($"Error getting secure key for {name}", e); } }
private static string CompileExpr(this Expression This) { if (This is BinaryExpression) { var bin = (BinaryExpression)This; var leftExpr = bin.Left.CompileExpr(); var rightExpr = bin.Right.CompileExpr(); if (rightExpr == "NULL" && bin.NodeType == ExpressionType.Equal) { if (bin.NodeType == ExpressionType.Equal) { return($"({leftExpr} IS NULL)"); } else if (rightExpr == "NULL" && bin.NodeType == ExpressionType.NotEqual) { return($"({leftExpr} IS NOT NULL)"); } } return($"({leftExpr} {GetSqlName(bin)} {rightExpr})"); } else if (This is ParameterExpression) { var param = (ParameterExpression)This; return($":{param.Name}"); } else if (This is MemberExpression) { var member = (MemberExpression)This; if (member.Expression != null && member.Expression.NodeType == ExpressionType.Parameter) { // This is a column in the table, output the column name var tableName = TableMapping.Get(member.Expression.Type).TableName; var columnName = ((PropertyInfo)member.Member).GetColumnName(); return($"\"{tableName}\".\"{columnName}\""); } else { return(member.EvaluateExpression().ConvertToSQLiteValue().ToSqlString()); } } else if (This.NodeType == ExpressionType.Not) { var operandExpr = ((UnaryExpression)This).Operand; return($"NOT({operandExpr.CompileExpr()})"); } else if (This is ConstantExpression) { return(This.EvaluateExpression().ConvertToSQLiteValue().ToSqlString()); } else if (This is MethodCallExpression) { var call = (MethodCallExpression)This; var args = new String[call.Arguments.Count]; var obj = call.Object != null?call.Object.CompileExpr() : null; for (var i = 0; i < args.Length; i++) { args[i] = call.Arguments[i].CompileExpr(); } if (call.Method.Name == "Like" && args.Length == 2) { return($"({args[0]} LIKE {args[1]})"); } else if (call.Method.Name == "Contains" && args.Length == 2) { return($"({args[1]} IN {args[0]})"); } else if (call.Method.Name == "Contains" && args.Length == 1) { if (call.Object != null && call.Object.Type == typeof(string)) { return($"({obj} LIKE ('%' || {args[0]} || '%'))"); } else { return($"({args[0]} IN {obj})"); } } else if (call.Method.Name == "StartsWith" && args.Length == 1) { return($"({obj} LIKE ({args[0]} || '%'))"); } else if (call.Method.Name == "EndsWith" && args.Length == 1) { return($"({obj} LIKE ('%' || {args[0]}))"); } else if (call.Method.Name == "Equals" && args.Length == 1) { return($"({obj} = ({args[0]}))"); } else if (call.Method.Name == "Is" && args.Length == 2) { return($"({args[0]} IS {args[1]})"); } else if (call.Method.Name == "IsNot" && args.Length == 2) { return($"({args[0]} IS NOT {args[1]})"); } } else if (This.NodeType == ExpressionType.Convert) { var u = (UnaryExpression)This; // Let SQLite handle the casting for us var operand = u.Operand; return(operand.CompileExpr()); // FIXME: Might still want to support a direct cast here if the // operand is a constant value or a function that is evaluated /* * var ty = u.Type; * var value = EvaluateExpression(u.Operand); * * return value.ConvertTo(ty).ConvertToSQLiteValue().ToSqlString();*/ } else if (This.NodeType == ExpressionType.Default) { var d = (DefaultExpression)This; var ty = d.Type; if (ty == typeof(void)) { return(""); } } else if (This.NodeType == ExpressionType.Lambda) { var expr = (LambdaExpression)This; return(CompileExpr(expr.Body)); } throw new NotSupportedException($"Cannot compile: {This.NodeType.ToString()}"); }
/// <summary> /// Try get by classifier /// </summary> public static IIdentifiedEntity TryGetExisting(this IIdentifiedEntity me, LocalDataContext context, bool forceDbSearch = false) { // Is there a classifier? var idpInstance = LocalPersistenceService.GetPersister(me.GetType()) as ILocalPersistenceService; if (idpInstance == null) { return(null); } //if (me.Key?.ToString() == "e4d3350b-b0f5-45c1-80ba-49e3844cbcc8") // System.Diagnostics.Debugger.Break(); IIdentifiedEntity existing = null; if (me.Key != null) { existing = context.TryGetCacheItem(me.Key.Value); } if (existing != null) { return(existing); } else if (!context.Connection.IsInTransaction) { existing = context.TryGetData(me.Key.Value.ToString()) as IdentifiedData; } else if (me.Key.HasValue) { existing = context.FindTransactedItem(me.Key.Value); } if (forceDbSearch && me.Key.HasValue) { ApplicationContext.Current.GetService <IDataCachingService>().Remove(me.Key.Value); } // Is the key not null? if (me.Key != Guid.Empty && me.Key != null && existing == null) { existing = idpInstance.Get(context, me.Key.Value) as IIdentifiedEntity; } var classAtt = me.GetType().GetTypeInfo().GetCustomAttribute <KeyLookupAttribute>(); if (classAtt != null && existing == null) { // Get the domain type var dataType = LocalPersistenceService.Mapper.MapModelType(me.GetType()); var tableMap = TableMapping.Get(dataType); // Get the classifier attribute value var classProperty = me.GetType().GetRuntimeProperty(classAtt.UniqueProperty); object classifierValue = classProperty.GetValue(me); // Get the classifier // Is the classifier a UUID'd item? if (classifierValue is IIdentifiedEntity) { classifierValue = (classifierValue as IIdentifiedEntity).Key.Value; classProperty = me.GetType().GetRuntimeProperty(classProperty.GetCustomAttribute <SerializationReferenceAttribute>()?.RedirectProperty ?? classProperty.Name); } // Column var column = tableMap.GetColumn(LocalPersistenceService.Mapper.MapModelProperty(me.GetType(), dataType, classProperty)); // Now we want to query SqlStatement stmt = new SqlStatement().SelectFrom(dataType) .Where($"{column.Name} = ?", classifierValue).Build(); var mapping = context.Connection.GetMapping(dataType); var dataObject = context.Connection.Query(mapping, stmt.SQL, stmt.Arguments.ToArray()).FirstOrDefault(); if (dataObject != null) { existing = idpInstance.ToModelInstance(dataObject, context) as IIdentifiedEntity; } } if (existing != null && me.Key.HasValue) { context.AddData(me.Key.Value.ToString(), existing); } return(existing); }
/// <summary> /// Try get by classifier /// </summary> public static IIdentifiedEntity TryGetExisting(this IIdentifiedEntity me, DataContext context, IPrincipal principal, bool forceDatabase = false) { // Is there a classifier? var idpInstance = AdoAuditPersistenceService.GetPersister(me.GetType()) as IAdoPersistenceService; var cacheService = ApplicationServiceContext.Current.GetService <IDataCachingService>(); IIdentifiedEntity existing = null; // Forcing from database load from if (forceDatabase && me.Key.HasValue) { // HACK: This should really hit the database instead of just clearing the cache ApplicationServiceContext.Current.GetService <IDataCachingService>()?.Remove(me.Key.Value); } //var tableType = AdoPersistenceService.GetMapper().MapModelType(me.GetType()); //if (me.GetType() != tableType) //{ // var tableMap = TableMapping.Get(tableType); // var dbExisting = context.FirstOrDefault(tableType, context.CreateSqlStatement().SelectFrom(tableType).Where($"{tableMap.Columns.FirstOrDefault(o=>o.IsPrimaryKey).Name}=?", me.Key.Value)); // if (dbExisting != null) // existing = idpInstance.ToModelInstance(dbExisting, context, principal) as IIdentifiedEntity; //} if (me.Key != Guid.Empty && me.Key != null) { existing = idpInstance.Get(context, me.Key.Value) as IIdentifiedEntity; } var classAtt = me.GetType().GetCustomAttribute <KeyLookupAttribute>(); if (classAtt != null && existing == null) { // Get the domain type var dataType = AdoAuditPersistenceService.GetMapper().MapModelType(me.GetType()); var tableMap = TableMapping.Get(dataType); // Get the classifier attribute value var classProperty = me.GetType().GetProperty(classAtt.UniqueProperty); object classifierValue = classProperty.GetValue(me); // Get the classifier // Is the classifier a UUID'd item? if (classifierValue is IIdentifiedEntity) { classifierValue = (classifierValue as IIdentifiedEntity).Key.Value; classProperty = me.GetType().GetProperty(classProperty.GetCustomAttribute <SerializationReferenceAttribute>()?.RedirectProperty ?? classProperty.Name); } // Column var column = tableMap.GetColumn(AdoAuditPersistenceService.GetMapper().MapModelProperty(me.GetType(), dataType, classProperty)); // Now we want to query SqlStatement stmt = context.CreateSqlStatement().SelectFrom(dataType) .Where($"{column.Name} = ?", classifierValue); Guid objIdCache = Guid.Empty; IDbIdentified dataObject = null; // We've seen this before String classKey = $"{dataType}.{classifierValue}"; if (m_classIdCache.TryGetValue(classKey, out objIdCache)) { existing = cacheService?.GetCacheItem(objIdCache) as IdentifiedData ?? context.GetCacheCommit(objIdCache); } if (existing == null) { dataObject = context.FirstOrDefault(dataType, stmt) as IDbIdentified; if (dataObject != null) { lock (m_classIdCache) if (!m_classIdCache.ContainsKey(classKey)) { m_classIdCache.Add(classKey, dataObject.Key); } var existCache = cacheService?.GetCacheItem((dataObject as IDbIdentified).Key); if (existCache != null) { existing = existCache as IdentifiedData; } else { existing = idpInstance.ToModelInstance(dataObject, context) as IIdentifiedEntity; } } } } return(existing); }
/// <summary> /// Hack the particular query /// </summary> public bool HackQuery(QueryBuilder builder, SqlStatement sqlStatement, SqlStatement whereClause, Type tmodel, PropertyInfo property, String queryPrefix, QueryPredicate predicate, object values, IEnumerable <TableMapping> scopedTables) { // Hack mnemonic queries if (typeof(Concept).GetTypeInfo().IsAssignableFrom(property.PropertyType.GetTypeInfo()) && predicate.SubPath == "mnemonic") { // Has this already been joined? var mapType = property.DeclaringType; if (mapType.GetTypeInfo().IsAbstract) { mapType = tmodel; } var declType = TableMapping.Get(this.m_mapper.MapModelType(mapType)); var keyProperty = property.PropertyType == typeof(Guid) ? property : mapType.GetRuntimeProperty(property.Name + "Key"); var declProp = declType.GetColumn(this.m_mapper.MapModelProperty(mapType, declType.OrmType, keyProperty)); if (declProp.ForeignKey == null) { return(false); // No FK link } var tblMap = TableMapping.Get(this.m_mapper.MapModelType(property.PropertyType)); var fkTbl = TableMapping.Get(declProp.ForeignKey.Table); string directFkName = $"{queryPrefix}{fkTbl.TableName}"; // We have to join to the FK table if (!declProp.IsAlwaysJoin) { var fkColumn = fkTbl.GetColumn(declProp.ForeignKey.Column); sqlStatement.Append($" INNER JOIN {fkTbl.TableName} AS {directFkName}_{declProp.Name} ON ({queryPrefix}{declType.TableName}.{declProp.Name} = {directFkName}_{declProp.Name}.{fkColumn.Name})"); directFkName += $"_{declProp.Name}"; } // We aren't yet joined to our table, we need to join to our table though!!!! if (declProp.ForeignKey.Table != tblMap.OrmType) { var fkKeyColumn = fkTbl.Columns.FirstOrDefault(o => o.ForeignKey?.Table == tblMap.OrmType && o.Name == tblMap.PrimaryKey.First().Name) ?? tblMap.Columns.FirstOrDefault(o => o.ForeignKey?.Table == fkTbl.OrmType && o.Name == fkTbl.PrimaryKey.First().Name); if (fkKeyColumn == null) { return(false); // couldn't find the FK link } // Now we want to filter our FK var tblName = $"{queryPrefix}{declProp.Name}_{tblMap.TableName}"; sqlStatement.Append($" INNER JOIN {tblMap.TableName} AS {tblName} ON ({directFkName}.{fkKeyColumn.Name} = {tblName}.{fkKeyColumn.Name})"); // Append the where clause whereClause.And(builder.CreateWhereCondition(property.PropertyType, predicate.SubPath, values, $"{queryPrefix}{declProp.Name}_", new List <TableMapping>() { tblMap }, tblName)); } else { // Append the where clause whereClause.And(builder.CreateWhereCondition(property.PropertyType, predicate.SubPath, values, $"{queryPrefix}{declProp.Name}_", new List <TableMapping>() { tblMap }, $"{directFkName}")); } return(true); } else { return(false); } }
/// <summary> /// Query internal for versioned data elements /// </summary> protected override IEnumerable <Object> DoQueryInternal(DataContext context, Expression <Func <TModel, bool> > primaryQuery, Guid queryId, int offset, int?count, out int totalResults, ModelSort <TModel>[] orderBy, bool countResults = true, bool overrideFuzzyTotalSetting = false) { #if DEBUG Stopwatch sw = new Stopwatch(); sw.Start(); #endif // Query has been registered? if (queryId != Guid.Empty && this.m_queryPersistence?.IsRegistered(queryId) == true) { return(this.GetStoredQueryResults(queryId, offset, count, out totalResults)); } // Queries to be performed IOrmResultSet retVal = null; Expression <Func <TModel, bool> >[] queries = new Expression <Func <TModel, bool> >[] { primaryQuery }; // Are we intersecting? if (context.Data.TryGetValue("UNION", out object others) && others is Expression <Func <TModel, bool> >[]) { context.Data.Remove("UNION"); queries = queries.Concat((Expression <Func <TModel, bool> >[])others).ToArray(); } try { // Execute queries foreach (var q in queries) { var query = q; // Is obsoletion time already specified? (this is important for versioned objects if we want to get the most current version of the object) if (!query.ToString().Contains("ObsoletionTime") && !query.ToString().Contains("VersionKey")) { var obsoletionReference = Expression.MakeBinary(ExpressionType.Equal, Expression.MakeMemberAccess(query.Parameters[0], typeof(TModel).GetProperty(nameof(BaseEntityData.ObsoletionTime))), Expression.Constant(null)); query = Expression.Lambda <Func <TModel, bool> >(Expression.MakeBinary(ExpressionType.AndAlso, obsoletionReference, query.Body), query.Parameters); } SqlStatement domainQuery = null; var expr = this.m_settingsProvider.GetMapper().MapModelExpression <TModel, TDomain, bool>(query, false); // Fast query? if (orderBy?.Length > 0 || q.ToString().Contains("VersionKey")) { if (expr != null) { domainQuery = context.CreateSqlStatement <TDomain>().SelectFrom(typeof(TDomain), typeof(TDomainKey)) .InnerJoin <TDomain, TDomainKey>(o => o.Key, o => o.Key) .Where <TDomain>(expr).Build(); } else { domainQuery = this.m_settingsProvider.GetQueryBuilder().CreateQuery(query).Build(); } // Create or extend queries if (retVal == null) { retVal = this.DomainQueryInternal <CompositeResult <TDomain, TDomainKey> >(context, domainQuery); } else { retVal = retVal.Union(this.DomainQueryInternal <CompositeResult <TDomain, TDomainKey> >(context, domainQuery)); } } else { var linkCol = TableMapping.Get(typeof(TDomain)).GetColumn(typeof(TDomain).GetProperty(nameof(DbIdentified.Key))); var versionSeqCol = TableMapping.Get(typeof(TDomain)).GetColumn(typeof(TDomain).GetProperty(nameof(DbVersionedData.VersionSequenceId))); if (expr != null) { domainQuery = context.CreateSqlStatement <TDomain>().SelectFrom(linkCol, versionSeqCol) .InnerJoin <TDomain, TDomainKey>(o => o.Key, o => o.Key) .Where <TDomain>(expr).Build(); } else { domainQuery = this.m_settingsProvider.GetQueryBuilder().CreateQuery(query, linkCol, versionSeqCol).Build(); } // Create or extend queries if (retVal == null) { retVal = this.DomainQueryInternal <Guid>(context, domainQuery); } else { retVal = retVal.Union(this.DomainQueryInternal <Guid>(context, domainQuery)); } } } // HACK: More than one query which indicates union was used, we need to wrap in a select statement to be adherent to SQL standard on Firebird and PSQL if (queries.Count() > 1) { var query = this.AppendOrderBy(context.CreateSqlStatement("SELECT * FROM (").Append(retVal.ToSqlStatement()).Append(") AS domain_query "), orderBy); retVal = this.DomainQueryInternal <Guid>(context, query); } else { this.AppendOrderBy(retVal.Statement, orderBy); } // Only perform count if (count == 0) { totalResults = retVal.Count(); return(new List <CompositeResult <TDomain, TDomainKey> >()); } else { if (context.Data.TryGetValue("principal", out object pRaw)) { var currentPrincipal = pRaw as IPrincipal; countResults = countResults && currentPrincipal != AuthenticationContext.SystemPrincipal; overrideFuzzyTotalSetting = currentPrincipal == AuthenticationContext.SystemPrincipal; } if (queryId != Guid.Empty && ApplicationServiceContext.Current.GetService <IQueryPersistenceService>() != null) { var keys = retVal.Keys <Guid>().Take(count.Value * 20).OfType <Guid>().ToArray(); totalResults = keys.Length; this.m_queryPersistence?.RegisterQuerySet(queryId, keys, queries, totalResults); if (totalResults == count.Value * 20) // result set is larger than 10,000 load in background { ApplicationServiceContext.Current.GetService <IThreadPoolService>().QueueUserWorkItem(o => { var dynParm = o as dynamic; var subContext = dynParm.Context as DataContext; var statement = dynParm.Statement as SqlStatement; var type = dynParm.Type as Type; var qid = (Guid)dynParm.QueryId; try { // Get the rest of the keys Guid[] sk = null; if (type == typeof(OrmResultSet <Guid>)) { sk = subContext.Query <Guid>(statement).ToArray(); } else { sk = subContext.Query <CompositeResult <TDomain, TDomainKey> >(statement).Keys <Guid>(false).ToArray(); } this.m_queryPersistence?.RegisterQuerySet(queryId, sk, statement, sk.Length); } finally { subContext.Dispose(); } }, new { Context = context.OpenClonedContext(), Statement = retVal.Statement.Build(), QueryId = queryId, Type = retVal.GetType() }); } return(keys.Skip(offset).Take(count.Value).OfType <Object>()); } else if (count.HasValue && countResults && !overrideFuzzyTotalSetting && !this.m_settingsProvider.GetConfiguration().UseFuzzyTotals) { totalResults = retVal.Count(); } else { totalResults = 0; } // Fuzzy totals - This will only fetch COUNT + 1 as the total results if (count.HasValue) { if ((overrideFuzzyTotalSetting || this.m_settingsProvider.GetConfiguration().UseFuzzyTotals) && totalResults == 0) { var fuzzResults = retVal.Skip(offset).Take(count.Value + 1).OfType <Object>().ToList(); totalResults = fuzzResults.Count() + offset; return(fuzzResults.Take(count.Value)); } else // We already counted as part of the queryId so no need to take + 1 { return(retVal.Skip(offset).Take(count.Value).OfType <Object>()); } } else { return(retVal.Skip(offset).OfType <Object>()); } } } catch (Exception ex) { if (retVal != null) { this.m_tracer.TraceEvent(EventLevel.Error, context.GetQueryLiteral(retVal.ToSqlStatement())); } throw new DataPersistenceException("Error executing query", ex); } finally { #if DEBUG sw.Stop(); // this.m_tracer.TraceVerbose("Query {0} in {1} ms", context.GetQueryLiteral(retVal.ToSqlStatement()), sw.ElapsedMilliseconds); #endif } }
public void TestCreate() { var table = TableMapping.Get <TestObjectWithAutoIncrementPrimaryKeyAndDefaultTableName>(); Assert.Equal(table.TableName, "TestObjectWithAutoIncrementPrimaryKeyAndDefaultTableName"); var expectedColumns = new string[] { "Id", "Uri", "B", "NotNull", "Collated", "Value", "AFloat", "NullableInt" }; Assert.Equal(expectedColumns, table.Columns.Keys); var expectedIndexes = new Dictionary <string, IndexInfo> { { "TestObjectWithAutoIncrementPrimaryKeyAndDefaultTableName_Uri", new IndexInfo(false, new string[] { "Uri" }) }, { "TestObjectWithAutoIncrementPrimaryKeyAndDefaultTableName_B", new IndexInfo(true, new string[] { "B" }) }, { "TestObjectWithAutoIncrementPrimaryKeyAndDefaultTableName_NotNull_Collated", new IndexInfo(false, new string[] { "NotNull", "Collated" }) }, { "named", new IndexInfo(true, new string[] { "Uri", "B" }) } }; var indexes = table.Indexes; Assert.Equal(indexes, expectedIndexes); var idMapping = table.Columns["Id"]; // Subtle touch, but nullables are ignored in the CLR type. Assert.Equal(idMapping.ClrType, typeof(Nullable <long>)); // No way to test the PropertyInfo directly Assert.Equal(idMapping.Metadata.CollationSequence, "BINARY"); Assert.Equal(idMapping.Metadata.DeclaredType, "INTEGER"); Assert.True(idMapping.Metadata.HasNotNullConstraint); Assert.True(idMapping.Metadata.IsAutoIncrement); Assert.True(idMapping.Metadata.IsPrimaryKeyPart); var uriMapping = table.Columns["Uri"]; Assert.Equal(uriMapping.ClrType, typeof(Uri)); // No way to test the PropertyInfo directly Assert.Equal(uriMapping.Metadata.CollationSequence, "BINARY"); Assert.Equal(uriMapping.Metadata.DeclaredType, "TEXT"); Assert.False(uriMapping.Metadata.HasNotNullConstraint); Assert.False(uriMapping.Metadata.IsAutoIncrement); Assert.False(uriMapping.Metadata.IsPrimaryKeyPart); var bMapping = table.Columns["B"]; Assert.Equal(bMapping.ClrType, typeof(string)); // No way to test the PropertyInfo directly Assert.Equal(bMapping.Metadata.CollationSequence, "BINARY"); Assert.Equal(bMapping.Metadata.DeclaredType, "TEXT"); Assert.False(bMapping.Metadata.HasNotNullConstraint); Assert.False(bMapping.Metadata.IsAutoIncrement); Assert.False(bMapping.Metadata.IsPrimaryKeyPart); Assert.False(table.Columns.ContainsKey("Ignored")); var notNullMapping = table.Columns["NotNull"]; Assert.Equal(notNullMapping.ClrType, typeof(byte[])); // No way to test the PropertyInfo directly Assert.Equal(notNullMapping.Metadata.CollationSequence, "BINARY"); Assert.Equal(notNullMapping.Metadata.DeclaredType, "BLOB"); Assert.True(notNullMapping.Metadata.HasNotNullConstraint); Assert.False(notNullMapping.Metadata.IsAutoIncrement); Assert.False(notNullMapping.Metadata.IsPrimaryKeyPart); var collatedMapping = table.Columns["Collated"]; Assert.Equal(collatedMapping.ClrType, typeof(string)); // No way to test the PropertyInfo directly Assert.Equal(collatedMapping.Metadata.CollationSequence, "Fancy Collation"); Assert.Equal(collatedMapping.Metadata.DeclaredType, "TEXT"); Assert.False(collatedMapping.Metadata.HasNotNullConstraint); Assert.False(collatedMapping.Metadata.IsAutoIncrement); Assert.False(collatedMapping.Metadata.IsPrimaryKeyPart); var valueMapping = table.Columns["Value"]; Assert.Equal(valueMapping.ClrType, typeof(DateTime)); // No way to test the PropertyInfo directly Assert.Equal(valueMapping.Metadata.CollationSequence, "BINARY"); Assert.Equal(valueMapping.Metadata.DeclaredType, "INTEGER"); Assert.True(valueMapping.Metadata.HasNotNullConstraint); Assert.False(valueMapping.Metadata.IsAutoIncrement); Assert.False(valueMapping.Metadata.IsPrimaryKeyPart); var aFloatMapping = table.Columns["AFloat"]; Assert.Equal(aFloatMapping.ClrType, typeof(float)); // No way to test the PropertyInfo directly Assert.Equal(aFloatMapping.Metadata.CollationSequence, "BINARY"); Assert.Equal(aFloatMapping.Metadata.DeclaredType, "REAL"); Assert.True(aFloatMapping.Metadata.HasNotNullConstraint); Assert.False(aFloatMapping.Metadata.IsAutoIncrement); Assert.False(aFloatMapping.Metadata.IsPrimaryKeyPart); var nullable = table.Columns["NullableInt"]; Assert.Equal(nullable.ClrType, typeof(Nullable <int>)); // No way to test the PropertyInfo directly Assert.Equal(nullable.Metadata.CollationSequence, "BINARY"); Assert.Equal(nullable.Metadata.DeclaredType, "INTEGER"); Assert.False(nullable.Metadata.HasNotNullConstraint); Assert.False(nullable.Metadata.IsAutoIncrement); Assert.False(nullable.Metadata.IsPrimaryKeyPart); }
public void TestCreateWithExplicitTableName() { var tableWithExplicitName = TableMapping.Get <TestObjectWithExplicitTableName>(); Assert.Equal(tableWithExplicitName.TableName, "ExplicitTableName"); }
/// <summary> /// Execute the current operation /// </summary> public IEnumerable <object> Execute(SubscriptionDefinition subscription, NameValueCollection parameters, int offset, int?count, out int totalResults, Guid queryId) { if (subscription == null || subscription.ServerDefinitions.Count == 0) { throw new InvalidOperationException("Subscription does not have server definition"); } try { var preArgs = new QueryRequestEventArgs <IdentifiedData>(o => o.Key == subscription.Key, offset, count, queryId, AuthenticationContext.Current.Principal, new ModelSort <IdentifiedData> [0], parameters); this.Executing?.Invoke(this, preArgs); if (preArgs.Cancel) { this.m_tracer.TraceWarning("Pre-Event for executor failed"); totalResults = preArgs.TotalResults; return(preArgs.Results); } var persistenceType = typeof(IDataPersistenceService <>).MakeGenericType(subscription.ResourceType); var persistenceInstance = ApplicationServiceContext.Current.GetService(persistenceType) as IAdoPersistenceService; var queryService = ApplicationServiceContext.Current.GetService <IQueryPersistenceService>(); var cacheService = ApplicationServiceContext.Current.GetService <IDataCachingService>(); // Get the definition var definition = subscription.ServerDefinitions.FirstOrDefault(o => o.InvariantName == m_configuration.Provider.Invariant); if (definition == null) { throw new InvalidOperationException($"Subscription does not provide definition for provider {m_configuration.Provider.Invariant}"); } // No obsoletion time? if (typeof(IBaseEntityData).IsAssignableFrom(subscription.ResourceType) && !parameters.ContainsKey("obsoletionTime")) { parameters.Add("obsoletionTime", "null"); } // Query expression var queryExpression = typeof(QueryExpressionParser).GetGenericMethod( nameof(QueryExpressionParser.BuildLinqExpression), new Type[] { subscription.ResourceType }, new Type[] { typeof(NameValueCollection) } ).Invoke(null, new object[] { parameters }); // Query has been registered? IEnumerable <IdentifiedData> result = null; if (queryId != Guid.Empty && queryService?.IsRegistered(queryId) == true) { totalResults = (int)queryService.QueryResultTotalQuantity(queryId); result = queryService.GetQueryResults(queryId, offset, count ?? 100) .Select(o => { try { var retVal = cacheService.GetCacheItem(o); if (retVal == null) { using (var ctx = m_configuration.Provider.GetReadonlyConnection()) { ctx.Open(); ctx.LoadState = LoadState.FullLoad; retVal = persistenceInstance.Get(ctx, o) as IdentifiedData; cacheService?.Add(retVal); } } return(retVal); } catch (Exception e) { this.m_tracer.TraceError("Error fetching query results for {0}: {1}", queryId, e); throw new DataPersistenceException("Error fetching query results", e); } }).OfType <IdentifiedData>().ToList(); } else { // Now grab the context and query!!! using (var connection = m_configuration.Provider.GetReadonlyConnection()) { try { connection.Open(); connection.LoadState = LoadState.FullLoad; // First, build the query using the query build TableMapping tableMapping = null; if (typeof(Entity).IsAssignableFrom(subscription.ResourceType)) { tableMapping = TableMapping.Get(typeof(DbEntityVersion)); } else if (typeof(Act).IsAssignableFrom(subscription.ResourceType)) { tableMapping = TableMapping.Get(typeof(DbActVersion)); } else if (typeof(Concept).IsAssignableFrom(subscription.ResourceType)) { tableMapping = TableMapping.Get(typeof(DbConceptVersion)); } else { throw new InvalidOperationException("ADO Subscriptions only support Entities and Acts (or sub-types)"); } var query = (typeof(QueryBuilder).GetGenericMethod( nameof(QueryBuilder.CreateQuery), new Type[] { subscription.ResourceType }, new Type[] { queryExpression.GetType(), typeof(ColumnMapping).MakeArrayType() } ).Invoke(this.m_queryBuilder, new object[] { queryExpression, tableMapping.Columns.ToArray() }) as SqlStatement).Build(); // Now we want to remove the portions of the built query statement after FROM and before WHERE as the definition will be the source of our selection SqlStatement domainQuery = new SqlStatement(m_configuration.Provider, query.SQL.Substring(0, query.SQL.IndexOf(" FROM "))); // Append our query var definitionQuery = definition.Definition; List <Object> values = new List <object>(); definitionQuery = this.m_parmRegex.Replace(definitionQuery, (o) => { if (parameters.TryGetValue("_" + o.Groups[2].Value.Substring(1, o.Groups[2].Value.Length - 2), out List <String> qValue)) { Guid uuid = Guid.Empty; if (Guid.TryParse(qValue.First(), out uuid)) { values.AddRange(qValue.Select(v => Guid.Parse(v)).OfType <Object>()); } else { values.AddRange(qValue); } return(o.Groups[1].Value + String.Join(",", qValue.Select(v => "?"))); } return("NULL"); }); // Now we want to append domainQuery.Append(" FROM (").Append(definitionQuery, values.ToArray()).Append($") AS {tableMapping.TableName} "); domainQuery.Append(query.SQL.Substring(query.SQL.IndexOf("WHERE ")), query.Arguments.ToArray()); // Now we want to create the result type var resultType = tableMapping.OrmType; if (typeof(IDbVersionedData).IsAssignableFrom(resultType)) // type is versioned so we have to join { var fkType = tableMapping.GetColumn("Key").ForeignKey.Table; resultType = typeof(CompositeResult <,>).MakeGenericType(resultType, fkType); } // Now we want to select out our results if (count == 0) { totalResults = connection.Count(domainQuery); return(null); } else { // Fetch var domainResults = typeof(DataContext).GetGenericMethod( nameof(DataContext.Query), new Type[] { resultType }, new Type[] { typeof(SqlStatement) }).Invoke(connection, new object[] { domainQuery }) as IOrmResultSet; IEnumerable <object> resultObjects = null; // Register query if query id specified if (queryId != Guid.Empty) { var results = domainResults.Keys <Guid>().OfType <Guid>().ToArray(); this.m_tracer.TraceVerbose("Query for Keys: {0}", connection.GetQueryLiteral(domainResults.Keys <Guid>().ToSqlStatement())); totalResults = results.Count(); ApplicationServiceContext.Current.GetService <IQueryPersistenceService>()?.RegisterQuerySet(queryId, results, null, totalResults); resultObjects = results.Skip(offset).Take(count ?? 100).OfType <Object>(); } else if (m_configuration.UseFuzzyTotals || preArgs.UseFuzzyTotals) { this.m_tracer.TraceVerbose("Query for Objects: {0}", connection.GetQueryLiteral(domainResults.ToSqlStatement())); resultObjects = domainResults.Skip(offset).Take((count ?? 100) + 1).OfType <Object>(); totalResults = domainResults.Count(); } else { this.m_tracer.TraceVerbose("Query for Objects: {0}", connection.GetQueryLiteral(domainResults.ToSqlStatement())); totalResults = domainResults.Count(); resultObjects = domainResults.Skip(offset).Take(count ?? 100).OfType <Object>(); } this.m_tracer.TraceVerbose("If i show up in the log, the log is ???????? WHY?????"); // Return result = resultObjects .Take(count ?? 100) .OfType <Object>() .Select(o => { try { if (o is Guid) { var retVal = cacheService.GetCacheItem((Guid)o); if (retVal == null) { using (var subConn = connection.OpenClonedContext()) { retVal = persistenceInstance.Get(subConn, (Guid)o) as IdentifiedData; cacheService?.Add(retVal); } } return(retVal); } else { var idData = (o as CompositeResult)?.Values.OfType <IDbIdentified>().FirstOrDefault() ?? o as IDbIdentified; var retVal = cacheService.GetCacheItem(idData.Key); if (retVal == null) { using (var subConn = connection.OpenClonedContext()) { retVal = persistenceInstance.ToModelInstance(o, subConn) as IdentifiedData; cacheService?.Add(retVal); } } return(retVal); } } catch (Exception e) { this.m_tracer.TraceError("Error converting result: {0}", e); throw; } }).OfType <IdentifiedData>().ToList(); } } catch (Exception e) { #if DEBUG this.m_tracer.TraceError("Error executing subscription: {0}", e); #else this.m_tracer.TraceError("Error executing subscription: {0}", e.Message); #endif throw new DataPersistenceException($"Error executing subscription: {e.Message}", e); } } // using conn } // if var postEvt = new QueryResultEventArgs <IdentifiedData>(o => o.Key == subscription.Key, result, offset, count, totalResults, queryId, AuthenticationContext.Current.Principal); this.Executed?.Invoke(this, postEvt); // Now set the overridden data return(postEvt.Results); } catch (Exception e) { this.m_tracer.TraceError("Error executing core ADO Subscription logic for {0}: {1}", subscription.Key, e); throw new Exception($"Error executing core ADO subscription logic for {subscription.Key}", e); } }
/// <summary> /// Perform the query /// </summary> protected virtual IEnumerable <Object> QueryInternal(DataContext context, Expression <Func <TModel, bool> > query, Guid queryId, int offset, int?count, out int totalResults, bool incudeCount = true) { #if DEBUG Stopwatch sw = new Stopwatch(); sw.Start(); #endif SqlStatement domainQuery = null; try { // Query has been registered? if (queryId != Guid.Empty && this.m_queryPersistence?.IsRegistered(queryId.ToString()) == true) { totalResults = (int)this.m_queryPersistence.QueryResultTotalQuantity(queryId.ToString()); var resultKeys = this.m_queryPersistence.GetQueryResults <Guid>(queryId.ToString(), offset, count.Value); return(resultKeys.Select(p => p.Id).OfType <Object>()); } // Is obsoletion time already specified? if (!query.ToString().Contains("ObsoletionTime") && typeof(BaseEntityData).IsAssignableFrom(typeof(TModel))) { var obsoletionReference = Expression.MakeBinary(ExpressionType.Equal, Expression.MakeMemberAccess(query.Parameters[0], typeof(TModel).GetProperty(nameof(BaseEntityData.ObsoletionTime))), Expression.Constant(null)); query = Expression.Lambda <Func <TModel, bool> >(Expression.MakeBinary(ExpressionType.AndAlso, obsoletionReference, query.Body), query.Parameters); } // Domain query domainQuery = context.CreateSqlStatement <TDomain>().SelectFrom(); var expression = m_mapper.MapModelExpression <TModel, TDomain>(query, false); if (expression != null) { Type lastJoined = typeof(TDomain); if (typeof(CompositeResult).IsAssignableFrom(typeof(TQueryReturn))) { foreach (var p in typeof(TQueryReturn).GenericTypeArguments.Select(o => AdoPersistenceService.GetMapper().MapModelType(o))) { if (p != typeof(TDomain)) { // Find the FK to join domainQuery.InnerJoin(lastJoined, p); lastJoined = p; } } } domainQuery.Where <TDomain>(expression); } else { m_tracer.TraceEvent(System.Diagnostics.TraceEventType.Verbose, 0, "Will use slow query construction due to complex mapped fields"); domainQuery = AdoPersistenceService.GetQueryBuilder().CreateQuery(query); } // Count = 0 means we're not actually fetching anything so just hit the db if (count != 0) { domainQuery = this.AppendOrderBy(domainQuery); // Query id just get the UUIDs in the db if (queryId != Guid.Empty && count != 0) { ColumnMapping pkColumn = null; if (typeof(CompositeResult).IsAssignableFrom(typeof(TQueryReturn))) { foreach (var p in typeof(TQueryReturn).GenericTypeArguments.Select(o => AdoPersistenceService.GetMapper().MapModelType(o))) { if (!typeof(DbSubTable).IsAssignableFrom(p) && !typeof(IDbVersionedData).IsAssignableFrom(p)) { pkColumn = TableMapping.Get(p).Columns.SingleOrDefault(o => o.IsPrimaryKey); break; } } } else { pkColumn = TableMapping.Get(typeof(TQueryReturn)).Columns.SingleOrDefault(o => o.IsPrimaryKey); } var keyQuery = AdoPersistenceService.GetQueryBuilder().CreateQuery(query, pkColumn).Build(); var resultKeys = context.Query <Guid>(keyQuery.Build()); //ApplicationContext.Current.GetService<IThreadPoolService>().QueueNonPooledWorkItem(a => this.m_queryPersistence?.RegisterQuerySet(queryId.ToString(), resultKeys.Select(o => new Identifier<Guid>(o)).ToArray(), query), null); // Another check this.m_queryPersistence?.RegisterQuerySet(queryId.ToString(), resultKeys.Count(), resultKeys.Select(o => new Identifier <Guid>(o)).Take(1000).ToArray(), query); ApplicationContext.Current.GetService <IThreadPoolService>().QueueNonPooledWorkItem(o => { int ofs = 1000; var rkeys = o as Guid[]; while (ofs < rkeys.Length) { this.m_queryPersistence?.AddResults(queryId.ToString(), rkeys.Skip(ofs).Take(1000).Select(k => new Identifier <Guid>(k)).ToArray()); ofs += 1000; } }, resultKeys.ToArray()); if (incudeCount) { totalResults = (int)resultKeys.Count(); } else { totalResults = 0; } var retVal = resultKeys.Skip(offset); if (count.HasValue) { retVal = retVal.Take(count.Value); } return(retVal.OfType <Object>()); } else if (incudeCount) { totalResults = context.Count(domainQuery); if (totalResults == 0) { return(new List <Object>()); } } else { totalResults = 0; } if (offset > 0) { domainQuery.Offset(offset); } if (count.HasValue) { domainQuery.Limit(count.Value); } return(this.DomainQueryInternal <TQueryReturn>(context, domainQuery, ref totalResults).OfType <Object>()); } else { totalResults = context.Count(domainQuery); return(new List <Object>()); } } catch (Exception ex) { if (domainQuery != null) { this.m_tracer.TraceEvent(TraceEventType.Error, ex.HResult, context.GetQueryLiteral(domainQuery.Build())); } context.Dispose(); // No longer important throw; } #if DEBUG finally { sw.Stop(); } #endif }
/// <summary> /// Query internal /// </summary> protected override IEnumerable <Object> QueryInternal(DataContext context, Expression <Func <TModel, bool> > query, Guid queryId, int offset, int?count, out int totalResults, bool countResults = true) { // Is obsoletion time already specified? if (!query.ToString().Contains("ObsoletionTime")) { var obsoletionReference = Expression.MakeBinary(ExpressionType.Equal, Expression.MakeMemberAccess(query.Parameters[0], typeof(TModel).GetProperty(nameof(BaseEntityData.ObsoletionTime))), Expression.Constant(null)); query = Expression.Lambda <Func <TModel, bool> >(Expression.MakeBinary(ExpressionType.AndAlso, obsoletionReference, query.Body), query.Parameters); } // Query has been registered? if (this.m_queryPersistence?.IsRegistered(queryId.ToString()) == true) { totalResults = (int)this.m_queryPersistence.QueryResultTotalQuantity(queryId.ToString()); var keyResults = this.m_queryPersistence.GetQueryResults <Guid>(queryId.ToString(), offset, count.Value); return(keyResults.Select(p => p.Id).OfType <Object>()); } SqlStatement domainQuery = null; try { domainQuery = context.CreateSqlStatement <TDomain>().SelectFrom() .InnerJoin <TDomain, TDomainKey>(o => o.Key, o => o.Key) .Where <TDomain>(m_mapper.MapModelExpression <TModel, TDomain>(query)).Build(); } catch (Exception e) { m_tracer.TraceEvent(System.Diagnostics.TraceEventType.Verbose, e.HResult, "Will use slow query construction due to {0}", e.Message); domainQuery = AdoPersistenceService.GetQueryBuilder().CreateQuery(query).Build(); } domainQuery = this.AppendOrderBy(domainQuery); // Query id just get the UUIDs in the db if (queryId != Guid.Empty) { ColumnMapping pkColumn = TableMapping.Get(typeof(TDomainKey)).Columns.SingleOrDefault(o => o.IsPrimaryKey); var keyQuery = AdoPersistenceService.GetQueryBuilder().CreateQuery(query, pkColumn).Build(); var resultKeys = context.Query <Guid>(keyQuery.Build()); this.m_queryPersistence?.RegisterQuerySet(queryId.ToString(), resultKeys.Count(), resultKeys.Take(1000).Select(o => new Identifier <Guid>(o)).ToArray(), query); ApplicationContext.Current.GetService <IThreadPoolService>().QueueNonPooledWorkItem(o => { int ofs = 1000; var rkeys = o as Guid[]; if (rkeys == null) { return; } while (ofs < rkeys.Length) { this.m_queryPersistence?.AddResults(queryId.ToString(), rkeys.Skip(ofs).Take(1000).Select(k => new Identifier <Guid>(k)).ToArray()); ofs += 1000; } }, resultKeys.ToArray()); if (countResults) { totalResults = (int)resultKeys.Count(); } else { totalResults = 0; } var retVal = resultKeys.Skip(offset); if (count.HasValue) { retVal = retVal.Take(count.Value); } return(retVal.OfType <Object>()); } else if (countResults) { totalResults = context.Count(domainQuery); if (totalResults == 0) { return(new List <CompositeResult <TDomain, TDomainKey> >()); } } else { totalResults = 0; } if (offset > 0) { domainQuery.Offset(offset); } if (count.HasValue) { domainQuery.Limit(count.Value); } return(context.Query <CompositeResult <TDomain, TDomainKey> >(domainQuery).OfType <Object>()); }
internal static string CompileFromClause(params Type[] types) => "FROM " + string.Join(", ", types.Select(x => "\"" + TableMapping.Get(x).TableName + "\""));
internal static string CompileJoinClause(bool left, Type table, Expression expr) { var tableName = TableMapping.Get(table).TableName; return((left ? "LEFT JOIN " : "INNER JOIN ") + "\"" + tableName + "\" ON " + expr.CompileExpr()); }