/* 
   EODatabaseChannel.m

   Copyright (C) 1996 Free Software Foundation, Inc.

   Author: Mircea Oancea <mircea@jupiter.elcom.pub.ro>
   Date: 1996

   This file is part of the GNUstep Database Library.

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Library General Public
   License as published by the Free Software Foundation; either
   version 2 of the License, or (at your option) any later version.

   This library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Library General Public License for more details.

   You should have received a copy of the GNU Library General Public
   License along with this library; see the file COPYING.LIB.
   If not, write to the Free Software Foundation,
   59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/

#include <eoaccess/common.h>

#include <Foundation/NSArray.h>
#include <Foundation/NSDictionary.h>
#include <Foundation/NSString.h>
#include <Foundation/NSDate.h>
#include <Foundation/NSUtilities.h>
#include <Foundation/NSObjCRuntime.h>

#include <extensions/NSException.h>
#include <extensions/exceptions/GeneralExceptions.h>
#include <extensions/objc-runtime.h>

#include <eoaccess/EOAdaptor.h>
#include <eoaccess/EOAdaptorContext.h>
#include <eoaccess/EOAdaptorChannel.h>
#include <eoaccess/EOModel.h>
#include <eoaccess/EOEntity.h>
#include <eoaccess/EOAttribute.h>
#include <eoaccess/EORelationship.h>
#include <eoaccess/EOQualifier.h>
#include <eoaccess/EONull.h>

#include "EODatabase.h"
#include "EODatabaseContext.h"
#include "EODatabaseChannel.h"
#include "EOObjectUniquer.h"
#include "EOGenericRecord.h"
#include "EOFault.h"

@class EOGenericRecord;

/*
 * Private methods declaration
 */

@interface EODatabaseChannel(Private)
- privateFetchWithZone:(NSZone*)aZone;
- (Class)privateClassForEntity:(EOEntity*)anEntity;
- (void)privateUpdateCurrentEntityInfo;
- (void)privateClearCurrentEntityInfo;
- (void)privateReportError:(SEL)method :(NSString*)format, ...;
@end

/*
 * EODatabaseChannel implementation
 */

@implementation EODatabaseChannel

/*
 * Initializing a new instance
 */

- initWithDatabaseContext:(EODatabaseContext*)aDatabaseContext
{
    adaptorChannel = [[[aDatabaseContext adaptorContext]
	createAdaptorChannel] retain];
    if (!aDatabaseContext || !adaptorChannel) {
	[self dealloc];
	return nil;
    }
    databaseContext = [aDatabaseContext retain];
    [databaseContext channelDidInit:self];
    return self;
}

- (void)dealloc
{
    [self closeChannel];
    [databaseContext channelWillDealloc:self];
    [databaseContext release];
    [adaptorChannel release];
    [super dealloc];
}

/*
 * Getting the adaptor channel
 */

- (EOAdaptorChannel*)adaptorChannel
{
    return adaptorChannel;
}

/*
 * Getting the database context
 */

- (EODatabaseContext*)databaseContext
{
    return databaseContext;
}

/*
 * Setting the delegate
 */

- (void)setDelegate:aDelegate
{
    delegate = aDelegate;
}

- delegate
{
    return delegate;
}

/*
 * Opening and closing a channel
 */

- (BOOL)isOpen
{
    return [adaptorChannel isOpen];
}

- (BOOL)openChannel
{
    return [adaptorChannel openChannel];
}

- (void)closeChannel
{
    [adaptorChannel closeChannel];
}

/*
 * Modifying objects
 */

- (BOOL)insertObject:anObj
{
    EOEntity* entity;
    NSDictionary* pkey;
    NSDictionary* values;
    NSDictionary* snapshot;
    NSArray* attributes;
    int i;
    
    // Check the delegate
    if ([delegate respondsToSelector:@selector(
	databaseChannel:willInsertObject:)])
	    anObj = [delegate databaseChannel:self willInsertObject:anObj];

    // Check nil (delegate disallowes or given object was nil)
    if (!anObj)
	return NO;

    // Check if fault
    if ([EOFault isFault:anObj])
	THROW([[InvalidArgumentException alloc] initWithFormat:
		    @"Attempt to insert a fault in a database channel"]);

    // Check if we can insert
    if ([databaseContext updateStrategy] == EONoUpdate) {
	[self privateReportError:_cmd : 
	    @"cannot insert if context has 'NoUpdate' update strategy."];
	return NO;
    }
    
    // Check if in a transaction
    if (![databaseContext transactionNestingLevel]) {
	[self privateReportError:_cmd : 
	    @"cannot insert if context has no transaction opened."];
	return NO;
    }
    
    // Get entity
    if ([anObj respondsToSelector:@selector(entity)])
	entity = [anObj entity];
    else
	entity = [[[[adaptorChannel adaptorContext] adaptor] model]
		    entityForObject:anObj];
    
    // Check entity
    if (!entity) {
	[self privateReportError:_cmd : 
	    @"cannot determine entity for object %p class %s.",
	    anObj, [(Object*)[anObj class] name]];
	return NO;
    }
    if ([entity isReadOnly]) {
	[self privateReportError:_cmd : 
	    @"cannot insert object %p for readonly entity %@.",
	    anObj, [entity name]];
	return NO;
    }

    // Get array of attributes to insert
    attributes = [entity attributesUsedForInsert];

    // Get simple values and convert them to adaptor values
    values = [entity convertValuesToModel:
		[anObj valuesForKeys:[entity attributesNamesUsedForInsert]]];
    
    // Get and check *must insert* attributes (primary key, lock, readonly)
    for (i = [attributes count]-1; i >= 0; i--) {
	if (![values objectForKey:
	    [(EOAttribute*)[attributes objectAtIndex:i] name]]) 
	{
	    [self privateReportError:_cmd : 
		@"null value for insert attribute %@ for object %p entity %@",
		[(EOAttribute*)[attributes objectAtIndex:i] name], anObj, 
		[entity name]];
	    return NO;
	}
    }
    
    // Make primary key and snapshot
    snapshot = [entity snapshotForRow:values];
    if (!snapshot) {
	[self privateReportError:_cmd : 
	    @"cannot determine snapshot for %p from values %@ entity %@.",
	    anObj, [values description], [entity name]];
	return NO;
    }
    pkey = [entity primaryKeyForRow:values];
    if (!pkey) {
	[self privateReportError:_cmd : 
	    @"cannot determine primary key for %p from values %@ entity %@.",
	    anObj, [values description], [entity name]];
	return NO;
    }
    
    // Insert adaptor row
    if (![adaptorChannel insertRow:values forEntity:entity])
	return NO;

    // Record object in database context
    [databaseContext recordObject:anObj 
	primaryKey:pkey entity:entity snapshot:values];
    
    // Notify delegate
    if ([delegate respondsToSelector:@selector(
	databaseChannel:didInsertObject:)])
	    [delegate databaseChannel:self didInsertObject:anObj];
    return YES;
}

- (BOOL)updateObject:anObj
{
    EOEntity* entity;
    EOQualifier* qualifier;
    NSDictionary* old_pkey;
    NSDictionary* old_snapshot;
    NSDictionary* new_pkey;
    NSDictionary* new_snapshot;
    NSDictionary* values;
    BOOL needsOptimisticLock;
    
    // Check the delegate
    if ([delegate respondsToSelector:@selector(
	databaseChannel:willUpdateObject:)])
	    anObj = [delegate databaseChannel:self willUpdateObject:anObj];

    // Check nil (delegate disallowes or given object was nil)
    if (!anObj)
	return NO;

    // Check if fault
    if ([EOFault isFault:anObj])
	THROW([[InvalidArgumentException alloc] initWithFormat:
		@"Attempt to update a fault in a database channel"]);

    // Check if we can update
    if ([databaseContext updateStrategy] == EONoUpdate) {
	[self privateReportError:_cmd : 
	    @"cannot update if context has 'NoUpdate' update strategy."];
	return NO;
    }
    
    // Check if in a transaction
    if (![databaseContext transactionNestingLevel]) {
	[self privateReportError:_cmd : 
	    @"cannot update if context has no transaction opened."];
	return NO;
    }
    
    // Get entity
    if ([anObj respondsToSelector:@selector(entity)])
	entity = [anObj entity];
    else
	entity = [[[[adaptorChannel adaptorContext] adaptor] model]
		    entityForObject:anObj];
    
    // Check entity
    if (!entity) {
	[self privateReportError:_cmd : 
	    @"cannot determine entity for object %p class %s.",
	    anObj, [(Object*)[anObj class] name]];
	return NO;
    }
    if ([entity isReadOnly]) {
	[self privateReportError:_cmd : 
	    @"cannot update object %p for readonly entity %@.",
	    anObj, [entity name]];
	return NO;
    }
    
    // Get and check old snapshot and primary key
    [databaseContext primaryKey:&old_pkey
	andSnapshot:&old_snapshot forObject:anObj];
    if (!old_snapshot) {
	[self privateReportError:_cmd : 
	    @"cannot update object %p because there is no snapshot for it."];
	return NO;
    }
    if (!old_pkey)
	old_pkey = [entity primaryKeyForRow:old_snapshot];
    if (!old_pkey) {
	[self privateReportError:_cmd : 
	    @"cannot determine primary key for %p from snapshot %@ entity %@.",
	    anObj, [old_snapshot description], [entity name]];
	return NO;
    }
    
    // Get simple values and convert them to adaptor values
    values = [entity convertValuesToModel:
		[anObj valuesForKeys:[entity attributesNamesUsedForInsert]]];
    
    // Get and check new primary key and snapshot
    new_snapshot = [entity snapshotForRow:values];
    if (!new_snapshot) {
	[self privateReportError:_cmd : 
	    @"cannot determine snapshot for %p from values %@ entity %@.",
	    anObj, [values description], [entity name]];
	return NO;
    }
    new_pkey = [entity primaryKeyForRow:new_snapshot];
    if (!new_pkey) {
	[self privateReportError:_cmd : 
	    @"cannot determine primary key for %p from values %@ entity %@.",
	    anObj, [values description], [entity name]];
	return NO;
    }
    
    // Check if we need to lock optimistic before update
    // that is compare locking attributes with the existing ones in database
    switch([databaseContext updateStrategy]) {
	case EOUpdateWithOptimisticLocking:
	case EOUpdateWithPessimisticLocking:
	    needsOptimisticLock = ![databaseContext isObjectLocked:anObj];
	    break;
	case EOUpdateWithNoLocking:
	    needsOptimisticLock = NO;
	    break;
	default:
	    return NO;
    }
        
    // If we need an "optimistic lock" then perform lock
    // else just make the qualifier based on the primary key only
    if (needsOptimisticLock) {
	int i;
	BOOL canUseQualifier = YES;
	NSArray* lockAttrs = [entity attributesUsedForLocking];
	EOAdaptor* adaptor = [[adaptorChannel adaptorContext] adaptor];
	NSDictionary* row;
	
	// Check if attributes used for locking can be used in a qualifier
	for (i = [lockAttrs count]-1; i >= 0; i--)
	    if (![adaptor isValidQualifierType:
		[[lockAttrs objectAtIndex:i] externalType]]) {
		canUseQualifier = NO;
		break;
	    }
	
	if (canUseQualifier)
	    // If YES just build the qualifier
	    qualifier = [EOQualifier qualifierForRow:old_snapshot 
			entity:entity];
	else {
	    // If NO then lock the row in the database server, fetch the
	    // new snapshot and compare it with the old one
	    qualifier = [EOQualifier qualifierForPrimaryKey:old_pkey 
			entity:entity];
	    if (![adaptorChannel selectAttributes:lockAttrs
		describedByQualifier:qualifier
		fetchOrder:nil 
		lock:YES]) {
		    [self privateReportError:_cmd : 
			@"could not lock %p with qualifier %@.",
			anObj, [qualifier description]];
		    return NO;
	    }
	    row = [adaptorChannel fetchAttributes:lockAttrs withZone:NULL];
	    [adaptorChannel cancelFetch];
	    if (!row) {
		[self privateReportError:_cmd : 
		    @"could not get row to lock %p with qualifier %@.",
		    anObj, [qualifier description]];
		return NO;
	    }
	    [databaseContext recordLockedObject:anObj];
	    if (![row isEqual:old_snapshot]) {
		[self privateReportError:_cmd : 
		    @"could not lock %p. Snapshots: self %@ database %@.",
		    anObj, [old_snapshot description], [row description]];
		return NO;
	    }
	}
    }
    else
	qualifier = [EOQualifier qualifierForPrimaryKey:old_pkey 
		    entity:entity];
    
    // Compute values as delta from values and old_snapshot
    {
	NSMutableDictionary* delta;
	NSString* attributeName;
	NSArray* allKeys;
	int i, count;
	
	allKeys = [values allKeys];
	delta = [NSMutableDictionary dictionary];
	for (i=0, count = [allKeys count]; i < count; i++) {
	    id new_v, old_v;

	    attributeName = [allKeys objectAtIndex:i];
	    new_v = [values objectForKey:attributeName];
	    old_v = [old_snapshot objectForKey:attributeName];
	    if (!old_v || ![new_v isEqual:old_v])
		[delta setObject:new_v forKey:attributeName];
	}
	values = delta;
    }
    
    // Update in adaptor
    if (![adaptorChannel updateRow:values describedByQualifier:qualifier])
	return NO;
    
    // Record object in database context
    // if (![new_pkey isEqual:old_pkey])
    //    [databaseContext forgetObject:anObj];
    [databaseContext recordObject:anObj 
	primaryKey:new_pkey
	entity:entity
	snapshot:new_snapshot];
    [databaseContext recordUpdatedObject:anObj];
    
    // Notify delegate
    if ([delegate respondsToSelector:@selector(
	databaseChannel:didUpdateObject:)])
	    [delegate databaseChannel:self didUpdateObject:anObj];
    return YES;
}

- (BOOL)deleteObject:anObj
{
    EOEntity* entity;
    NSDictionary* pkey;
    NSDictionary* snapshot;
    EOQualifier* qualifier;
    
    // Check the delegate
    if ([delegate respondsToSelector:@selector(
	databaseChannel:willDeleteObject:)])
	    anObj = [delegate databaseChannel:self willDeleteObject:anObj];

    // Check nil (delegate disallowes or given object was nil)
    if (!anObj)
	return NO;

    // Check if fault
    if ([EOFault isFault:anObj])
	THROW([[InvalidArgumentException alloc] initWithFormat:
		@"Attempt to delete a fault in a database channel"]);

    // Check if we can delete
    if ([databaseContext updateStrategy] == EONoUpdate) {
	[self privateReportError:_cmd : 
	    @"cannot delete if context has 'NoUpdate' update strategy."];
	return NO;
    }
    
    // Check if in a transaction
    if (![databaseContext transactionNestingLevel]) {
	[self privateReportError:_cmd : 
	    @"cannot update if context has no transaction opened."];
	return NO;
    }
        
    // Get entity
    if ([anObj respondsToSelector:@selector(entity)])
	entity = [anObj entity];
    else
	entity = [[[[adaptorChannel adaptorContext] adaptor] model]
		    entityForObject:anObj];
    
    // Check entity
    if (!entity) {
	[self privateReportError:_cmd : 
	    @"cannot determine entity for object %p class %s.",
	    anObj, [(Object*)[anObj class] name]];
	return NO;
    }
    if ([entity isReadOnly]) {
	[self privateReportError:_cmd : 
	    @"cannot delete object %p for readonly entity %@.",
	    anObj, [entity name]];
	return NO;
    }
    
    // Get snapshot and old primary key
    [databaseContext primaryKey:&pkey andSnapshot:&snapshot forObject:anObj];
    if (!pkey) {
	if (!snapshot)
	    [self privateReportError:_cmd : 
	    @"cannot delete object %p because there is no snapshot for it."];
	pkey = [entity primaryKeyForRow:snapshot];
    }
    if (!pkey) {
	[self privateReportError:_cmd : 
	    @"cannot determine primary key for %p from values %@ entity %@.",
	    anObj, [snapshot description], [entity name]];
	return NO;
    }
    
    // Get and check qualifier for object to delete
    qualifier = [EOQualifier qualifierForPrimaryKey:pkey entity:entity];
    if (!qualifier) {
	[self privateReportError:_cmd : 
	    @"cannot make qualifier to delete %p primary key %@ entity %@.",
	    anObj, [pkey description], [entity name]];
	return NO;
    }
    
    // Delete adaptor row
    if (![adaptorChannel deleteRowsDescribedByQualifier:qualifier])
	return NO;

    // Forget object in database context
    [databaseContext forgetObject:anObj];
    
    // Notify delegate
    if ([delegate respondsToSelector:@selector(
	databaseChannel:didDeleteObject:)])
	    [delegate databaseChannel:self didDeleteObject:anObj];
    return YES;
}

- (BOOL)lockObject:anObj
{
    EOEntity* entity;
    NSDictionary* pkey;
    NSDictionary* snapshot;
    EOQualifier* qualifier;
    
    // Check the delegate
    if ([delegate respondsToSelector:@selector(
	databaseChannel:willLockObject:)])
	    anObj = [delegate databaseChannel:self willLockObject:anObj];

    // Check nil (delegate disallowes or given object was nil)
    if (!anObj)
	return NO;

    // Check if fault
    if ([EOFault isFault:anObj])
	THROW([[InvalidArgumentException alloc] initWithFormat:
		    @"Attempt to lock a fault in a database channel"]);

    // Check if we can lock
    if ([databaseContext updateStrategy] == EONoUpdate) {
	[self privateReportError:_cmd : 
	    @"cannot lock if context has 'NoUpdate' update strategy."];
	return NO;
    }
    
    // Check if in a transaction
    if (![databaseContext transactionNestingLevel]) {
	[self privateReportError:_cmd : 
	    @"cannot lock if context has no transaction opened."];
	return NO;
    }
    
    // Check if fetch is in progress
    if ([self isFetchInProgress]) {
	[self privateReportError:_cmd : 
	    @"cannot lock if context has a fetch in progress."];
	return NO;
    }
    
    // Get entity
    if ([anObj respondsToSelector:@selector(entity)])
	entity = [anObj entity];
    else
	entity = [[[[adaptorChannel adaptorContext] adaptor] model]
		    entityForObject:anObj];
    
    // Check entity
    if (!entity) {
	[self privateReportError:_cmd : 
	    @"cannot determine entity for object %p class %s.",
	    anObj, [(Object*)[anObj class] name]];
	return NO;
    }
    if ([entity isReadOnly]) {
	[self privateReportError:_cmd : 
	    @"cannot lock object %p for readonly entity %@.",
	    anObj, [entity name]];
	return NO;
    }
    
    // Get snapshot and old primary key
    [databaseContext primaryKey:&pkey andSnapshot:&snapshot forObject:anObj];
    if (!snapshot) {
	[self privateReportError:_cmd : 
	    @"cannot lock object %p because there is no snapshot for it."];
	return NO;
    }
    if (!pkey) {
	pkey = [entity primaryKeyForRow:snapshot];
    }
    if (!pkey) {
	[self privateReportError:_cmd : 
	    @"cannot determine primary key for %p from values %@ entity %@.",
	    anObj, [snapshot description], [entity name]];
	return NO;
    }
    
    {
	NSArray* lockAttrs = [entity attributesUsedForLocking];
	NSDictionary* row;
	
	qualifier = [EOQualifier qualifierForPrimaryKey:pkey entity:entity];
	
	if (![adaptorChannel selectAttributes:lockAttrs
	    describedByQualifier:qualifier
	    fetchOrder:nil 
	    lock:YES]) {
		[self privateReportError:_cmd : 
		    @"could not lock %p with qualifier %@.",
		    anObj, [qualifier description]];
	    return NO;
	}
	row = [adaptorChannel fetchAttributes:lockAttrs withZone:NULL];
	[adaptorChannel cancelFetch];
	if (!row) {
	    [self privateReportError:_cmd : 
		@"could not lock %p with qualifier %@.",
		anObj, [qualifier description]];
	    return NO;
	}
	if (![row isEqual:snapshot]) {
	    [self privateReportError:_cmd : 
		@"could not lock %p. Snapshots: self %@ database %@.",
		anObj, [snapshot description], [row description]];
	    return NO;
	}
    }
    
    // Register lock object in database context
    [databaseContext recordLockedObject:anObj];
    
    // Notify delegate
    if ([delegate respondsToSelector:@selector(
	databaseChannel:didLockObject:)])
	    [delegate databaseChannel:self didLockObject:anObj];
    return YES;
}

- (BOOL)refetchObject:anObj
{
    EOEntity* entity;
    NSDictionary* pkey;
    NSDictionary* snapshot;
    EOQualifier* qualifier;
    
    // Check the delegate
    if ([delegate respondsToSelector:@selector(
	databaseChannel:willRefetchObject:)])
	    anObj = [delegate databaseChannel:self willRefetchObject:anObj];

    // Check nil (delegate disallowes or given object was nil)
    if (!anObj)
	return NO;

    // Check if fault
    if ([EOFault isFault:anObj])
	THROW([[InvalidArgumentException alloc] initWithFormat:
		@"Attempt to refetch a fault in a database channel"]);

    // Check if in a transaction
    if (![databaseContext transactionNestingLevel]) {
	[self privateReportError:_cmd : 
	    @"cannot refetch if context has no transaction opened."];
	return NO;
    }
    
    // Check if fetch is in progress
    if ([self isFetchInProgress]) {
	[self privateReportError:_cmd : 
	    @"cannot refetch if context has a fetch in progress."];
	return NO;
    }
    
    // Get entity
    if ([anObj respondsToSelector:@selector(entity)])
	entity = [anObj entity];
    else
	entity = [[[[adaptorChannel adaptorContext] adaptor] model]
		    entityForObject:anObj];
    
    // Check entity
    if (!entity) {
	[self privateReportError:_cmd : 
	    @"cannot determine entity for object %p class %s.",
	    anObj, [(Object*)[anObj class] name]];
	return NO;
    }
    
    // Get snapshot and old primary key
    [databaseContext primaryKey:&pkey andSnapshot:&snapshot forObject:anObj];
    if (!pkey) {
	if (!snapshot)
	    [self privateReportError:_cmd : 
	    @"cannot refetch object %p because there is no snapshot for it."];
	pkey = [entity primaryKeyForRow:snapshot];
    }
    if (!pkey) {
	[self privateReportError:_cmd : 
	    @"cannot determine primary key for %p from values %@ entity %@.",
	    anObj, [snapshot description], [entity name]];
	return NO;
    }
    
    // Get and check qualifier for object to refetch
    qualifier = [EOQualifier qualifierForPrimaryKey:pkey entity:entity];
    if (!qualifier) {
	[self privateReportError:_cmd : 
	    @"cannot make qualifier to refetch %p primary key %@ entity %@.",
	    anObj, [pkey description], [entity name]];
	return NO;
    }
    
    // Request object from adaptor
    [self setCurrentEntity:entity];
    [self privateUpdateCurrentEntityInfo];
    if (!currentAttributes) {
	[self privateReportError:_cmd : 
	    @"internal inconsitency while refetching %p.", anObj];
	return NO;
    }
    if (![adaptorChannel selectAttributes:currentAttributes
	    describedByQualifier:qualifier 
	    fetchOrder:nil
	    lock:([databaseContext updateStrategy] ==
		    EOUpdateWithPessimisticLocking)]) {
	[self privateClearCurrentEntityInfo];
	return NO;
    }

    // Get object from adaptor, re-build its faults and record new snapshot
    anObj = [self privateFetchWithZone:NULL];
    [self cancelFetch];
    if (!anObj) {
	[self privateReportError:_cmd : 
	    @"could not refetch %p with qualifier %@.",
	    anObj, [qualifier description]];
	return NO;
    }

    // Notify delegate
    if ([delegate respondsToSelector:@selector(
	databaseChannel:didRefetchObject:)])
	    [delegate databaseChannel:self didRefetchObject:anObj];
    return YES;
}

- _createObjectForRow:(NSDictionary*)aRow entity:(EOEntity*)anEntity 
  isPrimaryKey:(BOOL)yn zone:(NSZone*)zone
{
    Class class;
    id anObj;
    
    if (!anEntity)
	return nil;
    
    class = [self privateClassForEntity:anEntity];

    // Create new instance
    if ([class respondsToSelector:@selector(classForEntity:values:)])
	class = [class classForEntity:anEntity values:aRow];
    anObj = [class allocWithZone:zone];
    
    return anObj;
}

- (id)allocateObjectForRow:(NSDictionary*)row
  entity:(EOEntity*)anEntity
  zone:(NSZone*)zone
{
    Class class;
    id anObj;
    
    if (!anEntity)
	return nil;
    
    class = [self privateClassForEntity:anEntity];
    
    // Create new instance
    if ([class respondsToSelector:@selector(classForEntity:values:)])
	class = [class classForEntity:anEntity values:row];
    anObj = [class allocWithZone:zone];
    
    return anObj;
}

- (id)initializedObjectForRow:(NSDictionary*)row
  entity:(EOEntity*)anEntity
  zone:(NSZone*)zone
{
    id anObj = [self allocateObjectForRow:row entity:anEntity zone:zone];
    
    if ([anObj respondsToSelector:@selector(initWithPrimaryKey:entity:)])
	anObj = [anObj initWithPrimaryKey:row entity:anEntity];
    else
	anObj = [anObj init];
    
    return [anObj autorelease];
}

/*
 * Fetching objects
 */

- _fetchObject:anObj qualifier:(EOQualifier*)qualifier
{
    id obj;
    
    [self selectObjectsDescribedByQualifier:qualifier fetchOrder:nil];
    obj = [self fetchWithZone:NULL];
    [self cancelFetch];
    return obj;
}

- (BOOL)selectObjectsDescribedByQualifier:(EOQualifier*)qualifier
  fetchOrder:(NSArray*)fetchOrder
{
    if ([delegate respondsToSelector:@selector(
	databaseChannel:willSelectObjectsDescribedByQualifier:fetchOrder:)])
	    if (![delegate databaseChannel:self 
		willSelectObjectsDescribedByQualifier:qualifier
		fetchOrder:fetchOrder])
		    return NO;
 
    [self setCurrentEntity:[qualifier entity]];
    [self privateUpdateCurrentEntityInfo];
    if (!currentAttributes) {
	[self privateReportError:_cmd : 
	    @"internal inconsitency while selecting."];
    }
    if (![adaptorChannel selectAttributes:currentAttributes
	    describedByQualifier:qualifier 
	    fetchOrder:fetchOrder
	    lock:([databaseContext updateStrategy] ==
		    EOUpdateWithPessimisticLocking)]) {
	[self privateClearCurrentEntityInfo];
	[self privateReportError:_cmd : 
	    @"could not select attributes with qualifier %@.",
	    [qualifier description]];
	return NO;
    }

    if ([delegate respondsToSelector:@selector(
	databaseChannel:didSelectObjectsDescribedByQualifier:fetchOrder:)])
	    [delegate databaseChannel:self 
		didSelectObjectsDescribedByQualifier:qualifier
		fetchOrder:fetchOrder];
    return YES;
}

- fetchWithZone:(NSZone*)zone
{
    id anObj;
    
    if ([delegate respondsToSelector:@selector(
	databaseChannel:willFetchObjectOfClass:withZone:)])
	    [delegate databaseChannel:self 
		willFetchObjectOfClass:(currentClass ? currentClass :
			[self privateClassForEntity:currentEntity])
		withZone:zone];
    anObj = [self privateFetchWithZone:zone];
    if (!anObj)
	return nil;
    if ([delegate respondsToSelector:@selector(
	databaseChannel:didFetchObject:)])
	    [delegate databaseChannel:self didFetchObject:anObj];
    return anObj;
}

- (BOOL)isFetchInProgress
{
    return [adaptorChannel isFetchInProgress];
}

- (void)cancelFetch
{
    if ([adaptorChannel isFetchInProgress]) {
	[self privateClearCurrentEntityInfo];
	[adaptorChannel cancelFetch];
    }
}

- (void)setCurrentEntity:(EOEntity*)anEntity
{
    // Clear entity info
    [self privateClearCurrentEntityInfo];
    // Set new entity
    currentEntity = [anEntity retain];
}

- (void)privateClearCurrentEntityInfo
{
    [currentEntity release];
    currentEntity = nil;
    [currentAttributes release];
    currentAttributes = nil;
    [currentRelations release];
    currentRelations = nil;
    currentClass = Nil;
    currentReady = NO;
}

- (void)privateUpdateCurrentEntityInfo
{
    if (!currentEntity) {
	THROW([[InvalidArgumentException alloc] initWithFormat:
	@"Must use setCurrentEntity if select is not done through database"]);
    }
    if (!currentAttributes)
	currentAttributes = [[currentEntity attributesUsedForFetch] retain];
    if (!currentRelations)
	currentRelations = [[currentEntity relationsUsedForFetch] retain];
    currentReady = YES;
}

/*
 * Private methods
 */

- (Class)privateClassForEntity:(EOEntity*)anEntity
{
    Class class;
    
    if (anEntity == currentEntity && currentClass)
	return currentClass;
    
    // Get class for new object
    class = NSClassFromString([anEntity className]);

    if (!class && [delegate respondsToSelector:
			@selector(databaseChannel:failedToLookupClassNamed:)])
	class = [delegate databaseChannel:self 
	    failedToLookupClassNamed:[[anEntity className] cString]];
    if (!class)
	class = [EOGenericRecord class];

    if (anEntity == currentEntity)
	currentClass = class;
    
    return class;
}

- privateFetchWithZone:(NSZone*)aZone
{
    id anObj;
    NSDictionary* pkey;
    NSDictionary* snapshot;
    NSDictionary* row;
    NSDictionary* dict;
    NSMutableDictionary* values;
    
    // Be sure we have entity info (raises if no entity is set)
    if (!currentReady)
	[self privateUpdateCurrentEntityInfo];
    
    // fetch row from adaptor
    row = [adaptorChannel fetchAttributes:currentAttributes withZone:aZone];
    if (!row)
	// Results set finished or no more result sets
	return nil;
    
    // determine primary key and snapshot
    snapshot = [currentEntity snapshotForRow:row];
    pkey = [currentEntity primaryKeyForRow:row];
    if (!pkey || !snapshot)
	// TODO - should we have a delegate method here ?
	THROW([[InvalidArgumentException alloc] initWithFormat:
		    @"Cannot determine primary key and snapshot for row"]);
    
    // lookup object in context/database
    anObj = [databaseContext objectForPrimaryKey:pkey entity:currentEntity];
    
    // use old, make new, clear fault
    if (!anObj) {
	anObj = [self initializedObjectForRow:row
	    entity:currentEntity
	    zone:aZone];
    }
    if ([EOFault isFault:anObj]) {
	[EOFault clearFault:anObj];
        if ([anObj respondsToSelector:@selector(initWithPrimaryKey:entity:)])
	    anObj = [anObj initWithPrimaryKey:row entity:currentEntity];
	else
	    anObj = [anObj init];
	if (!anObj) {
	    THROW([[InvalidArgumentException alloc] initWithFormat:
	    @"could not initialize cleared fault with row `%@' and entity %@",
	    [row description], [currentEntity name]]);
	}
    }
    
    // make values
    // TODO - handle only class properties to object
    values = [NSMutableDictionary dictionaryWithCapacity:
	[row count]+[currentRelations count]];
    [values addEntriesFromDictionary:row];
    
    // resolve relationships (to-one and to-many)
    {
	int i, n = [currentRelations count];
	EORelationship* rel;
	id fault = nil;
	
	for (i=0; i<n; i++) {
	    rel = [currentRelations objectAtIndex:i];
	    // Check if the delegate can provide a different relationship
	    if ([delegate respondsToSelector:@selector(
		databaseChannel:relationshipForRow:relationship:)]) {
		id nrel = [delegate databaseChannel:self
		    relationshipForRow:row relationship:rel];
		rel = nrel ? nrel : rel;
	    }
	    if ([rel isToMany]) {
		// Build to-many fault
		EOQualifier* qualifier = [EOQualifier 
		    qualifierForRow:row relationship:rel];
		if (!qualifier)
		    //THROW([[InvalidArgumentException alloc] initWithFormat:
		    //@"Cannot build fault qualifier for relationship"]);
		    //    TODO    
		    continue;
		if ([NSClassFromString([[rel destinationEntity] className])
			isGarbageCollectable])
		    fault = [EOFault gcArrayFaultWithQualifier:qualifier
			fetchOrder:nil
			databaseChannel:self
			zone:aZone];
		else
		    fault = [EOFault arrayFaultWithQualifier:qualifier
			fetchOrder:nil
			databaseChannel:self
			zone:aZone];
	    }
	    else {
		// Build to-one fault
		EOEntity* faultEntity = [rel destinationEntity];
		NSDictionary* faultKey = [faultEntity primaryKeyForRow:
		    [rel foreignKeyForRow:row]];
		
		if (!faultEntity)
		    THROW([[InvalidArgumentException alloc] initWithFormat:
				@"Cannot get entity for relationship"]);
		if (faultKey) {
		    fault = [databaseContext objectForPrimaryKey:faultKey
			entity:faultEntity];
		    if (!fault) {
			fault = [EOFault objectFaultWithPrimaryKey:faultKey
			    entity:faultEntity
			    databaseChannel:self
			    zone:aZone];
			[databaseContext recordObject:fault 
			    primaryKey:faultKey
			    entity:faultEntity
			    snapshot:nil];
		    }
		}
		else
		    fault = [EONull null];
	    }
	    if (fault)
		[values setObject:fault forKey:[rel name]];
	}
    }
    
    // check if is updated in another context or just updated or new (delegate)
    dict = values;
    if ([[databaseContext database] isObject:anObj 
	    updatedOutsideContext:databaseContext]) {
	if ([delegate respondsToSelector:@selector(
	    databaseChannel:willRefetchConflictingObject:withSnapshot:)]) {
	    dict = [delegate databaseChannel:self
		willRefetchConflictingObject:anObj
		withSnapshot:values];
	}
	else
	    THROW([[InvalidArgumentException alloc] initWithFormat:
		@"object updated in an uncommitted transaction was fetched"]);
    }
    else {
	if ([delegate respondsToSelector:@selector(
	    databaseChannel:willRefetchObject:fromSnapshot:)]) {
	    dict = [delegate databaseChannel:self
		willRefetchObject:anObj
		fromSnapshot:values];
	}
    }
    // does delegate disallow setting the new values and recording the fetch ?
    if (!dict)
	return anObj;
    
    // put values
    [anObj takeValuesFromDictionary:dict];
    
    // register lock if locked
    if ([databaseContext updateStrategy] == EOUpdateWithPessimisticLocking)
	[databaseContext recordLockedObject:anObj];
    
    // register object in context
    [databaseContext recordObject:anObj
	primaryKey:pkey
	entity:currentEntity
	snapshot:snapshot];
    
    // awake object from database channel
    if ([anObj respondsToSelector:@selector(awakeForDatabaseChannel:)])
	[anObj awakeForDatabaseChannel:self];
    
    // Done.
    return anObj;
}

/*
 * Reporting errors
 */

- (void)privateReportError:(SEL)method :(NSString*)format,...
{
    NSString* message;
    va_list va;
    
    if (![[databaseContext database] logsErrorMessages])
	return;
    
    va_start(va, format);
    message = [[[NSString alloc] initWithFormat:format arguments:va] 
	autorelease];
    va_end(va);
    
    [[databaseContext database] reportErrorFormat:
	@"EODatabaseChannel:%x in [EODatabaseChannel %@]: %@", self,
	NSStringFromSelector(method), message];
}

@end /* EODatabaseChannel */

