/* 
   Postgres95Channel.m

   Copyright (C) 1996 Free Software Foundation, Inc.

   Author: Mircea Oancea <mircea@jupiter.elcom.pub.ro>
	      based on the Sybase Adaptor written by 
	   Ovidiu Predescu <ovidiu@bx.logicnet.ro>
   Date: December 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 <Foundation/NSArray.h>
#include <Foundation/NSDictionary.h>
#include <Foundation/NSString.h>
#include <Foundation/NSObjCRuntime.h>
#include <Foundation/NSUtilities.h>

#include <extensions/NSException.h>
#include <extensions/exceptions/GeneralExceptions.h>

#include <eoaccess/common.h>
#include <eoaccess/EOModel.h>
#include <eoaccess/EOEntity.h>
#include <eoaccess/EOAttribute.h>
#include <eoaccess/EONull.h>
#include <eoaccess/EOCustomValues.h>
#include <eoaccess/EOSQLExpression.h>
#include <eoaccess/EOQualifier.h>
#include <eoaccess/exceptions/EOFExceptions.h>

#include <eoadaptors/Postgres95/Postgres95Channel.h>
#include <eoadaptors/Postgres95/Postgres95Exceptions.h>
#include <eoadaptors/Postgres95/Postgres95Values.h>

static void __dummy_function_used_for_linking (void)
{
    extern __postgres95_values_linking_function (void);

    __postgres95_values_linking_function ();
    __dummy_function_used_for_linking ();
}

@implementation Postgres95Channel

- initWithAdaptorContext:(EOAdaptorContext*)_adaptorContext
{
    [super initWithAdaptorContext:_adaptorContext];
    ASSIGN(adaptor, (Postgres95Adaptor*)[_adaptorContext adaptor]);
    oidToTypeName = [[NSMutableDictionary alloc] initWithCapacity:101];
    return self;
}

- (void)dealloc
{
    if([self isOpen])
	[self closeChannel];
    [adaptor release];
    [sqlExpression release];
    [oidToTypeName release];
    [super dealloc];
}

- (BOOL)openChannel
{
    if(![super openChannel])
	return NO;
    pgConn = [adaptor newPGconn];
    if (pgConn)
	[self _describeDatabaseTypes];
    return (pgConn != NULL);
}

- (void)closeChannel
{
    [super closeChannel];
    [self cancelResults];
    [adaptor releasePGconn:pgConn force:NO];
    pgConn = NULL;
}

- (PGconn*)pgConn
{
    return pgConn;
}

- (PGresult*)pgResult
{
    return pgResult;
}

- (void)cancelFetch
{
    [super cancelFetch];
    [self cancelResults];
}

- (void)cancelResults
{
    if (pgResult) {
	PQclear(pgResult);
	pgResult = NULL;
	currentResultRow = -2;
    }
}

- (BOOL)advanceRow
{
    // fetch results where read then freed
    if (pgResult == NULL)
	return NO;
    
    // next row
    currentResultRow++;

    // check if result set is finished
    if (currentResultRow >= PQntuples(pgResult)) {
	[self cancelResults];
	return NO;
    }

    return YES;	
}

- (BOOL)_evaluateCommandsUntilAFetch
{
    BOOL ret = NO;
    ExecStatusType status;
        
    // Check results
    switch ((status = PQresultStatus(pgResult))) {
	case PGRES_EMPTY_QUERY:
	    isFetchInProgress = NO;
	    ret = YES;
	    break;
	case PGRES_COMMAND_OK:
	    isFetchInProgress = NO;
	    ret = YES;
	    break;
	case PGRES_TUPLES_OK:
	    isFetchInProgress = YES;
	    currentResultRow = -1;
	    ret = YES;
	    break;
	case PGRES_COPY_OUT:
	    isFetchInProgress = NO;
	    ret = YES;
	    break;
	case PGRES_COPY_IN:
	    isFetchInProgress = NO;
	    ret = YES;
	    break;
	case PGRES_BAD_RESPONSE:
	case PGRES_NONFATAL_ERROR:
	case PGRES_FATAL_ERROR: 
	{
	    NSString* errorString = [NSString stringWithFormat:
		@"SQL expression '%@' caused %s", 
		sqlExpression, PQerrorMessage(pgConn)];
	    [adaptor reportError:errorString];
	    return NO;
	}
	default:
	    THROW([[Postgres95Exception alloc] initWithFormat:
		@"unexpected result retuned by PQresultStatus()"]);
    }
    
    if (ret == YES) {
	PGnotify* notify = PQnotifies(pgConn);
	const char* insoid;
	
	if (notify) {
	    if (postgres95DelegateRespondsTo.postgres95Notification)
		[delegate postgres95Channel:self
		    receivedNotification:
			[NSString stringWithCString:notify->relname]];
	    free(notify);
	}
	
	insoid = PQoidStatus(pgResult);
	if (*insoid && postgres95DelegateRespondsTo.postgres95InsertedRowOid) {
	    Oid oid = atol(insoid);
	    [delegate postgres95Channel:self insertedRowWithOid:oid];
	}
    }
    
    if ([self isDebugEnabled]) {
	id message = [NSString stringWithCString:PQcmdStatus(pgResult)];
	if (status == PGRES_TUPLES_OK)
	    message = [NSString stringWithFormat:
		@"Command status %@. Returned %d rows with %d columns ",
		message, PQntuples(pgResult), PQnfields(pgResult)];
	NSLog (@"Postgres95Adaptor: %@", message);
    }

    return ret;
}

- (BOOL)evaluateExpression:(NSString*)expression
{
    expression = [[expression mutableCopy] autorelease];
    ASSIGN(sqlExpression, expression);
    if(delegateRespondsTo.willEvaluateExpression) {
	EODelegateResponse response
	    = [delegate adaptorChannel:self
			willEvaluateExpression:(NSMutableString*)expression];
	if(response == EODelegateRejects)
	    return NO;
	else if(response == EODelegateOverrides)
	    return YES;
    }

    if ([self isOpen] == NO)
	    THROW([[Postgres95Exception alloc] initWithFormat:
		@"cannot execute SQL expression. Channel is not opened."]);
    
    if ([self isDebugEnabled])
	NSLog (@"Postgres95Adaptor: execute command:\n%@\n", expression);

    /* cancel results from previous query */
    [self cancelResults];

    /* Send the expression to the SQL server */
    pgResult = PQexec(pgConn, (char*)[expression cString]);
    if (pgResult == NULL) {
	[adaptor privateReportError:pgConn];
	return NO;
    }

    /* Check command results */
    if ([self _evaluateCommandsUntilAFetch] == NO)
	return NO;

    if(delegateRespondsTo.didEvaluateExpression)
	[delegate adaptorChannel:self didEvaluateExpression:expression];

    return YES;
}

- (NSMutableDictionary*)primaryFetchAttributes:(NSArray*)attributes 
  withZone:(NSZone*)zone
{
    NSMutableDictionary* row;
    int i, count = [attributes count];
    BOOL owned;
    
    if ([self advanceRow] == NO)
	// Return nil to indicate that the fetch operation was finished
	return nil;
    
    row = [[[NSMutableDictionary allocWithZone:zone]
		initWithCapacity:count]
		autorelease];
    
    if (count > PQnfields(pgResult))
	THROW([[Postgres95Exception alloc] initWithFormat:
	    @"attempt to read %d attributes "
	    @"when the result set has only %d columns",
	    count, PQnfields(pgResult)]);

    for (i = 0; i < count; i++) {
	EOAttribute* attr = [attributes objectAtIndex:i];
	NSString* attrName = [attr name];
	NSString* valueClassName;
	Class class;
	int length;
	const char* string;
	id value;

	// If the column has the NULL value insert EONull in row
	if (PQgetisnull(pgResult, currentResultRow, i)) {
	    [row setObject:[EONull null] forKey:attrName];
	    continue;
	}
	string = PQgetvalue(pgResult, currentResultRow, i);
	length = PQgetlength(pgResult, currentResultRow, i);
	valueClassName = [attr valueClassName];
	class = NSClassFromString (valueClassName);

	if (!class)
	    THROW([(UnknownClassException*)[UnknownClassException alloc]
			setClassName:valueClassName]);
	
	// if external type for this attribute is "inversion" then this
	// column represents an Oid of a large object
	if ([[attr externalType] isEqual:@"inversion"]) {
	    string = [self _readBinaryDataRow:(Oid)atol(string)
			length:&length zone:zone];
	    owned = YES;
	}
	else {
	    owned = NO;
	}
	value = [class postgres95ValueFromBytes:(char*)string
			length:length
			owned:owned
			attribute:attr
			adaptorChannel:self
			zone:zone];
	if (value == nil)
	    THROW([[DataTypeMappingNotSupportedException alloc]
		    initWithFormat:@"Postgres95 could make not make value "
		    @"from class %@ for attribute %@ entity",
		    [attr valueClassName], [attr name], [[attr entity] name]]);
	[row setObject:value forKey:attrName];
    }

    return row;
}

/* We have to override the insertRow:forEntity: method because of the way in
   which the Postgres95 libpq works with binary data. Very ugly! */
- (BOOL)insertRow:(NSDictionary*)row forEntity:(EOEntity*)entity
{
    EOSQLExpression* sqlexpr;
    NSMutableDictionary* mrow;
    NSMutableDictionary* nrow;
    NSEnumerator* enumerator;
    NSString* attrName;

    if(!row || !entity)
	THROW([[InvalidArgumentException alloc]
	    initWithFormat:@"row and entity arguments for insertRow:forEntity:"
			   @" must not be the nil object"]);

    if([self isFetchInProgress])
	THROW([AdaptorIsFetchingException exceptionWithAdaptor:self]);

    if(![adaptorContext transactionNestingLevel])
	THROW([NoTransactionInProgressException exceptionWithAdaptor:self]);

    mrow = [[row mutableCopyWithZone:[row zone]] autorelease];

    if(delegateRespondsTo.willInsertRow) {
	EODelegateResponse response;

	response = [delegate adaptorChannel:self
			     willInsertRow:mrow
			     forEntity:entity];
	if(response == EODelegateRejects)
	    return NO;
	else if(response == EODelegateOverrides)
	    return YES;
    }

    /* Before creating the SQL INSERT expression we have to replace in the
       row the large objects as Oids and to insert them with the large
       object file-like interface */

    nrow = [mrow mutableCopy];
    enumerator = [mrow keyEnumerator];
    while ((attrName = [enumerator nextObject])) {
	EOAttribute* attribute = [entity attributeNamed:attrName];
	NSString* externalType;

	if (!attribute)
	    return NO;

	externalType = [attribute externalType];

	/* Insert the binary value into the binaryDataRow dictionary */
	if ([externalType isEqual:@"inversion"]) {
	    id binValue = [mrow objectForKey:attrName];
	    Oid binOid = [self _insertBinaryData:binValue 
			forAttribute:attribute];
	    
	    [nrow setObject:[NSNumber numberWithLong:binOid] forKey:attrName];
	}
    }

    if ([nrow count]) {
	sqlexpr = [[[adaptorContext adaptor] expressionClass]
			insertExpressionForRow:nrow
			entity:entity
			channel:self];
	if(![self evaluateExpression:[sqlexpr expressionValueForContext:nil]])
	    return NO;
    }

    if(delegateRespondsTo.didInsertRow)
	[delegate adaptorChannel:self didInsertRow:mrow forEntity:entity];

    return YES;
}

/* We have to do a similar thing like with -insertRow:forEntity: because of the
   Postgres95 large object interface. Plus the postgres library does not return
   the count of updated records - since we have to return NO if no record is 
   actually updated we have to count the records involved in update here. */
- (BOOL)updateRow:(NSDictionary*)row
  describedByQualifier:(EOQualifier*)qualifier
{
    EOSQLExpression* sqlexpr;
    EOEntity* entity = [qualifier entity];
    NSMutableDictionary* mrow;
    NSMutableArray* invAttributes;
    NSEnumerator* enumerator;
    NSString* attrName;
    NSString* externalType;
    EOAttribute* attr;
    
    if(!row)
	THROW([[InvalidArgumentException alloc]
	    initWithFormat:@"row argument for updateRow:describedByQualifier: "
			   @"must not be the nil object"]);

    if([self isFetchInProgress])
	THROW([AdaptorIsFetchingException exceptionWithAdaptor:self]);

    if(![adaptorContext transactionNestingLevel])
	THROW([NoTransactionInProgressException exceptionWithAdaptor:self]);

    mrow = [[row mutableCopyWithZone:[row zone]] autorelease];
    if(delegateRespondsTo.willUpdateRow) {
	EODelegateResponse response;

	response = [delegate adaptorChannel:self
			     willUpdateRow:mrow
			     describedByQualifier:qualifier];
	if(response == EODelegateRejects)
	    return NO;
	else if(response == EODelegateOverrides)
	    return YES;
    }

    if ([mrow count] == 0)
	return NO;

    // Get EOAttributes involved in update operation
    // Modify "inversion" attributes to NSNumber type with the Oid

    invAttributes = [[[NSMutableArray alloc] initWithCapacity:[mrow count]] 
		    autorelease];
    enumerator = [mrow keyEnumerator];
    while ((attrName = [enumerator nextObject])) {
	attr = [entity attributeNamed:attrName];
	externalType = [attr externalType];
	
	if (attr == nil)
	    return NO;
	
	if ([externalType isEqual:@"inversion"]) {
	    EOAttribute* invAttr = [[EOAttribute new] autorelease];
	    [invAttr setName:attrName];
	    [invAttr setEntity:entity];
	    [invAttr setColumnName:[attr columnName]];
	    [invAttr setExternalType:@"oid"];
	    [invAttr setValueType:@"l"];
	    [invAttr setValueClassName:@"NSNumber"];
	    [invAttributes addObject:invAttr];
	}
    }

    if ([invAttributes count] == 0) {

	// Select with update qualifier to see there is only one row
	// to be updated - there is a hack here based on the fact that
	// in update there in only one table and no flattened attributes

	EOAttribute* countAttr;	
	NSDictionary* countRow;
	unsigned long count;
	NSArray* countAttributes;

	countAttr = [[EOAttribute new] autorelease];
	[countAttr setName:@"count"];
	[countAttr setEntity:entity];
	[countAttr setExternalType:@"int4"];
	[countAttr setColumnName:@""];
	[countAttr setSelectFormat:@"count(*)"];
	[countAttr setValueType:@"L"];
	[countAttr setValueClassName:@"NSNumber"];
	countAttributes = [NSArray arrayWithObject:countAttr];
	
	sqlexpr = [[[adaptorContext adaptor] expressionClass]
			selectExpressionForAttributes:countAttributes
			lock:NO
			qualifier:qualifier
			fetchOrder:nil
			channel:self];
	if(![self evaluateExpression:[sqlexpr expressionValueForContext:nil]])
	    return NO;
	
	countRow = [self fetchAttributes:countAttributes withZone:NULL];
	[self cancelFetch];
	count = [[countRow objectForKey:@"count"] unsignedLongValue];
	if (count != 1)
	    // No row to update
	    return NO;
    }
    else {
	
	// Select with update qualifier to see there is only one row
	// to be updated and to get the large objects (to be updatetd)
	// Oid from dataserver - there is a hack here based on the fact that
	// in update there in only one table and no flattened attributes
	
	NSDictionary* firstRow;
	NSDictionary* secondRow;

	sqlexpr = [[[adaptorContext adaptor] expressionClass]
			selectExpressionForAttributes:invAttributes
			lock:NO
			qualifier:qualifier
			fetchOrder:nil
			channel:self];
	if(![self evaluateExpression:[sqlexpr expressionValueForContext:nil]])
	    return NO;

	firstRow = [self fetchAttributes:invAttributes withZone:NULL];
	secondRow = [self fetchAttributes:invAttributes withZone:NULL];
	[self cancelFetch];

	if (firstRow == nil)
	    // No row to update
	    return NO;

	if (secondRow != nil)
	    // Multiple rows to update
	    return NO;

    	// Update the large objects and modify the row to update with Oid's

	enumerator = [invAttributes objectEnumerator];
	while ((attr = [enumerator nextObject])) {
	    Oid oldOid, newOid;
	    NSData* data;
	    
	    attrName = [attr name];
	    data = [mrow objectForKey:attrName];
	    if ([data respondsToSelector:@selector(dataForType:)])
		data = [data dataForType:[attr valueType]];
	    else
		data = nil;
	    oldOid = [[firstRow objectForKey:attrName] longValue];
	    newOid = [self _updateBinaryDataRow:oldOid data:data];
	    
	    [mrow setObject:[NSNumber numberWithUnsignedLong:newOid] 
		  forKey:attrName];
	}
    }

    // Now we have all: one and only row to update and binary rows
    // (large objects) where updated and their new Oid set in the row

    if ([mrow count]) {
	sqlexpr = [[[adaptorContext adaptor] expressionClass]
			    updateExpressionForRow:mrow
			    qualifier:qualifier
			    channel:self];
	if(![self evaluateExpression:[sqlexpr expressionValueForContext:nil]])
	    return NO;
    }

    if (delegateRespondsTo.didUpdateRow)
	[delegate adaptorChannel:self
		  didUpdateRow:mrow
		  describedByQualifier:qualifier];
    return YES;
}

/* The binaryDataRow should contain only one binary attribute */
- (char*)_readBinaryDataRow:(Oid)oid length:(int*)length zone:(NSZone*)zone;
{
    int fd;
    int len, wrt;
    char* bytes;
    
    if (oid == 0) {
	*length = 0;
	return NULL;
    }
    
    fd = lo_open(pgConn, oid, INV_READ|INV_WRITE);
    if (fd < 0)
	THROW([[Postgres95Exception alloc] initWithFormat:
	    @"cannot open large object Oid = %ld", oid]);
    
    lo_lseek(pgConn, fd, 0, SEEK_END);
    len = lo_tell(pgConn, fd);
    lo_lseek(pgConn, fd, 0, SEEK_SET);
    
    if (len < 0)
	THROW([[Postgres95Exception alloc] initWithFormat:
	    @"error while getting size of large object Oid = %ld", oid]);
    
    bytes = NSZoneMalloc(zone, len);
    wrt = lo_read(pgConn, fd, bytes, len);
    
    if (len != wrt) {
	Free(bytes);
	THROW([[Postgres95Exception alloc] initWithFormat:
	    @"error while reading large object Oid = %ld", oid]);
    }
    lo_close(pgConn, fd);
    
    *length = len;
    return bytes;
}

- (Oid)_insertBinaryData:(NSData*)binaryData forAttribute:(EOAttribute*)attr
{
    int len;
    const char* bytes;
    Oid oid;
    int fd, wrt;
    
    if (binaryData == [EONull null] || binaryData == nil)
	return 0;
    
    len = [binaryData length];
    bytes = [binaryData bytes];

    oid = lo_creat(pgConn, INV_READ|INV_WRITE);
    if (oid == 0)
	THROW([[Postgres95Exception alloc] initWithFormat:
	    @"cannot create large object"]);

    fd = lo_open(pgConn, oid, INV_READ|INV_WRITE);
    if (fd < 0)
	THROW([[Postgres95Exception alloc] initWithFormat:
	    @"cannot open large object Oid = %ld", oid]);
    
    wrt = lo_write(pgConn, fd, (char*)bytes, len);
    
    if (len != wrt)
	THROW([[Postgres95Exception alloc] initWithFormat:
	    @"error while writing large object Oid = %ld", oid]);
    
    lo_close(pgConn, fd);
    
    return oid;
}

- (Oid)_updateBinaryDataRow:(Oid)oid data:(NSData*)binaryData
{
    int len;
    const char* bytes;
    int wrt, fd;
    
    if (oid)
	lo_unlink(pgConn, oid);
    
    if (binaryData == [EONull null] || binaryData == nil)
	return 0;
    
    len = [binaryData length];
    bytes = [binaryData bytes];

    oid = lo_creat(pgConn, INV_READ|INV_WRITE);
    if (oid == 0)
	THROW([[Postgres95Exception alloc] initWithFormat:
	    @"cannot create large object"]);

    fd = lo_open(pgConn, oid, INV_READ|INV_WRITE);
    if (fd < 0)
	THROW([[Postgres95Exception alloc] initWithFormat:
	    @"cannot open large object Oid = %ld", oid]);
    
    wrt = lo_write(pgConn, fd, (char*)bytes, len);
    
    if (len != wrt)
	THROW([[Postgres95Exception alloc] initWithFormat:
	    @"error while writing large object Oid = %ld", oid]);
    
    lo_close(pgConn, fd);
    
    return oid;
}

/* Read type oid and names from the database server. 
   Called on each openChannel to refresh info. */
- (void)_describeDatabaseTypes
{
    int i, count;
    
    pgResult = PQexec(pgConn, 
	"SELECT oid, typname FROM pg_type WHERE typrelid = 0");
    if (pgResult == NULL || PQresultStatus(pgResult) != PGRES_TUPLES_OK) {
	pgResult = NULL;
	THROW([[Postgres95Exception alloc] initWithFormat:
	    @"cannot read type name informations from database. "
	    @"bad response from server"]);
    }
    
    if (PQnfields(pgResult) != 2) {
	pgResult = NULL;
	THROW([[Postgres95Exception alloc] initWithFormat:
	    @"cannot read type name informations from database. "
	    @"results should have two columns"]);
    }
    
    [oidToTypeName removeAllObjects];
    count = PQntuples(pgResult);
    
    for (i = 0; i < count; i++) {
	char* oid = PQgetvalue(pgResult, i, 0);
	char* typ = PQgetvalue(pgResult, i, 1);
	
	[oidToTypeName setObject:[NSString stringWithCString:typ]
		       forKey:[NSNumber numberWithLong:atol(oid)]];
    }
    
    PQclear(pgResult);
    pgResult = NULL;
}

- (NSArray*)describeResults
{
    int i, colsNumber = pgResult ? PQnfields(pgResult): 0;
    id* attributes;

    if (![self isFetchInProgress])
	THROW ([AdaptorIsNotFetchingException new]);

    if (colsNumber == 0)
	return [[[NSArray alloc] init] autorelease];

    attributes = alloca (colsNumber * sizeof(id));

    for (i = 0; i < colsNumber; i++) {
	EOAttribute* attribute = [[EOAttribute new] autorelease];
	NSString* externalName;
	NSString* valueClass = @"NSString";
	NSString* valueType = nil;
	
	externalName = [oidToTypeName 
	    objectForKey:[NSNumber numberWithLong:PQftype(pgResult, i)]];

	if (externalName == nil)
	    THROW([[Postgres95Exception alloc] initWithFormat:
		@"cannot find type for Oid = %d", PQftype(pgResult, i)]);

	[attribute setName:[NSString stringWithFormat:@"attribute%d", i]];
	[attribute setColumnName:@"unknown"];
	[attribute setExternalType:externalName];

	if      ([externalName isEqual:@"bool"])
	    valueClass = @"NSNumber", valueType = @"c";
	else if ([externalName isEqual:@"char"])
	    valueClass = @"NSNumber", valueType = @"c";
	else if ([externalName isEqual:@"dt"])
	    valueClass = @"NSCalendarDate", valueType = nil;
	else if ([externalName isEqual:@"date"])
	    valueClass = @"NSCalendarDate", valueType = nil;
	else if ([externalName isEqual:@"time"])
	    valueClass = @"NSCalendarDate", valueType = nil;
	else if ([externalName isEqual:@"float4"])
	    valueClass = @"NSNumber", valueType = @"f";
	else if ([externalName isEqual:@"float8"])
	    valueClass = @"NSNumber", valueType = @"d";
	else if ([externalName isEqual:@"int4"])
	    valueClass = @"NSNumber", valueType = @"i";
	else if ([externalName isEqual:@"int8"])
	    valueClass = @"NSNumber", valueType = @"l";
	else if ([externalName isEqual:@"oid"])
	    valueClass = @"NSNumber", valueType = @"l";

	[attribute setValueType:valueType];
	[attribute setValueClassName:valueClass];
	attributes[i] = attribute;
    }

    return [[[NSArray alloc] initWithObjects:attributes count:colsNumber]
		autorelease];
}

/* The methods used to generate an model from the meta-information kept by
   the database. */

- (NSArray*)describeTableNames
{
    // TODO
    [self notImplemented:_cmd];
    return nil;
}

- (EOModel*)describeModelWithTableNames:(NSArray*)tableNames
{
    // TODO
    [self notImplemented:_cmd];
    return nil;
}

- (void)setDelegate:_delegate
{
    [super setDelegate:_delegate];
    postgres95DelegateRespondsTo.postgres95InsertedRowOid = 
	[delegate respondsToSelector:
	    @selector(postgres95Channel:insertedRowWithOid:)];
    postgres95DelegateRespondsTo.postgres95Notification = 
	[delegate respondsToSelector:
	    @selector(postgres95Channel:receivedNotification:)];
}

@end /* Postgres95Channel */
