/*
 * This is part of the Postgres module for Pike.
 *
 * $Id: postgres.pike,v 1.32 2008/07/05 11:17:45 srb Exp $
 *
 */

//! This is an interface to the Postgres (Postgres95, pgsql) database
//! server. This module may or may not be availible on your Pike,
//! depending whether the appropriate include and library files could
//! be found at compile-time. Note that you @b{do not@} need to have a
//! Postgres server running on your host to use this module: you can
//! connect to the database over a TCP/IP socket.
//!
//! @note
//! Also note that @b{this module uses blocking I/O@} I/O to connect
//! to the server. Postgres is quite slow, and so you might want to
//! consider this particular aspect. It is (at least should be)
//! thread-safe, and so it can be used in a multithread environment.
//!
//! The behavior of the Postgres C API also depends on certain
//! environment variables defined in the environment of the Pike
//! interpreter.
//!
//! @string
//!   @value "PGHOST"
//!     Sets the name of the default host to connect to. It defaults
//! 	to @expr{"localhost"@}.
//!   @value "PGOPTIONS"
//!     Sets some extra flags for the frontend-backend connection.
//!     @b{do not set@} unless you're sure of what you're doing.
//!   @value "PGPORT"
//!     Sets the default port to connect to, otherwise it will use
//!     compile-time defaults (that is: the time you compiled the postgres
//! 	library, not the Pike driver).
//!   @value "PGTTY"
//!     Sets the file to be used for Postgres frontend debugging.
//!     Do not use, unless you're sure of what you're doing.
//!   @value "PGDATABASE"
//!     Sets the default database to connect to.
//!   @value "PGREALM"
//!     Sets the default realm for Kerberos authentication. I never used
//!   	this, so I can't help you.
//! @endstring
//!
//! Refer to the Postgres documentation for further details.
//!
//! @seealso
//!  @[Sql.Sql], @[Postgres.postgres], @[Sql.postgres_result]

#pike __REAL_VERSION__

#if constant(Postgres.postgres)

#define ERROR(X) throw (({X,backtrace()}))

inherit Postgres.postgres: mo;
private protected mixed  callout;
private string has_relexpires = "unknown";

//! @decl void select_db(string dbname)
//!
//! This function allows you to connect to a database. Due to
//! restrictions of the Postgres frontend-backend protocol, you always
//! have to be connected to a database, so in fact this function just
//! allows you to connect to a different database on the same server.
//!
//! @note
//! This function @b{can@} raise exceptions if something goes wrong
//! (backend process not running, not enough permissions..)
//!
//! @seealso
//!   create

//! @decl string error()
//!
//! This function returns the textual description of the last
//! server-related error. Returns @expr{0@} if no error has occurred
//! yet. It is not cleared upon reading (can be invoked multiple
//! times, will return the same result until a new error occurs).
//!
//! @seealso
//!   big_query

//! @decl string host_info()
//!
//! This function returns a string describing what host are we talking to,
//! and how (TCP/IP or UNIX sockets).

//! @decl void reset()
//!
//! This function resets the connection to the backend. Can be used for
//! a variety of reasons, for example to detect the status of a connection.
//!
//! @note
//! This function is Postgres-specific, and thus it is not availible
//! through the generic SQL-interface.

//! @decl string version
//!
//! Should you need to report a bug to the author, please submit along with
//! the report the driver version number, as returned by this call.

private protected string glob_to_regexp (string glob) {
	if (!glob||!sizeof(glob))
		return 0;
	return "^"+replace(glob,({"*","?","'","\\"}),({".*",".","\\'","\\\\"}))+"$";
}

protected private int mkbool(string s) {
	if (s=="f")
		return 0;
	return 1;
}

//! @decl void create()
//! @decl void create(string host, void|string database, void|string user,@
//!                   void|string password)
//!
//! With no arguments, this function initializes (reinitializes if a
//! connection had been previously set up) a connection to the
//! Postgres backend. Since Postgres requires a database to be
//! selected, it will try to connect to the default database. The
//! connection may fail however for a variety of reasons, in this case
//! the most likely of all is because you don't have enough authority
//! to connect to that database. So use of this particular syntax is
//! discouraged.
//!
//! The host argument can have the syntax @expr{"hostname"@} or
//! @expr{"hostname:portname"@}. This allows to specify the TCP/IP
//! port to connect to. If it is @expr{0@} or @expr{""@}, it will try
//! to connect to localhost, default port.
//!
//! The database argument specifies the database to connect to. If
//! @expr{0@} or @expr{""@}, it will try to connect to the specified
//! database.
//!
//! The username and password arguments are silently ignored, since
//! the Postgres C API doesn't allow to connect to the server as any
//! user different than the user running the interface.
//!
//! @note
//! You need to have a database selected before using the sql-object,
//! otherwise you'll get exceptions when you try to query it. Also
//! notice that this function @b{can@} raise exceptions if the db
//! server doesn't respond, if the database doesn't exist or is not
//! accessible by you.
//!
//! You don't need bothering about syncronizing the connection to the database:
//! it is automatically closed (and the database is sync-ed) when the
//! object is destroyed.
//!
//! @seealso
//!   @[Postgres.postgres], @[Sql.Sql], @[postgres->select_db]
void create(void|string host, void|string database, void|string user,
		void|string _pass) {
	string pass = _pass;
	_pass = "CENSORED";
	string real_host=host, real_db=database;
	int port=0;
	quote = this->_quote ? this->_quote : simple_quote;
	if (stringp(host)&&(search(host,":")>=0))
		if (sscanf(host,"%s:%d",real_host,port)!=2)
			ERROR("Error in parsing the hostname argument.\n");
	
	mo::create(real_host||"",real_db||"",user||"",pass||"",port);
}

protected void poll (int delay)
{
	callout=call_out(poll,delay,delay);
	big_query("");
}

//! @decl void set_notify_callback()
//! @decl void set_notify_callback(function f)
//! @decl void set_notify_callback(function f, int|float poll_delay)
//!
//! With Postgres you can associate events and notifications to tables.
//! This function allows you to detect and handle such events.
//!
//! With no arguments, resets and removes any callback you might have
//! put previously, and any polling cycle.
//!
//! With one argument, sets the notification callback (there can be only
//! one for each sqlobject). 
//! 
//! With two arguments, sets a notification callback and sets a polling
//! cycle.
//!
//! The polling cycle is necessary because of the way notifications are
//! delivered, that is piggyback with a query result. This means that
//! if you don't do any query, you'll receive no notification. The polling
//! cycle starts a call_out cycle which will do an empty query when
//! the specified interval expires, so that pending notifications 
//! may be delivered.
//!
//! The callback function must return no value, and takes a string argument,
//! which will be the name of the table on which the notification event
//! has occured. In future versions, support for user-specified arguments
//! will be added.
//!
//! @note
//! The polling cycle can be run only if your process is in "event-driven mode"
//! (that is, if 'main' has returned a negative number).
//!
//! This function is Postgres-specific, and thus it is not availible
//! through the generic SQL-interface.
//!
//! @fixme
//! An integer can be passed as first argument, but it's effect is
//! not documented.
void set_notify_callback(int|function f, int|float|void poll_delay) {
	if (callout) {
		remove_call_out(callout);
		callout=0;
	}
	if (intp(f)) {
		mo::_set_notify_callback(0);
		return;
	}
	mo::_set_notify_callback(f);
	if(poll_delay>0) 
		poll(poll_delay);
}

function quote;
string simple_quote(string s)
{
  return replace(s, ({ "\\", "'", "\0" }), ({ "\\\\", "''", "\\0" }) );
}

//! This function creates a new database with the given name (assuming we
//! have enough permissions to do this).
//!
//! @seealso
//!   drop_db
void create_db(string db) {
	big_query(sprintf("CREATE DATABASE %s",db));
}

//! This function destroys a database and all the data it contains (assuming
//! we have enough permissions to do so).
//!
//! @seealso
//!   create_db
void drop_db(string db) {
	big_query(sprintf("DROP database %s",db));
}

//! This function returns a string describing the server we are
//! talking to. It has the form @expr{"servername/serverversion"@}
//! (like the HTTP protocol description) and is most useful in
//! conjunction with the generic SQL-server module.
string server_info () {
  return "Postgres/unknown";
}

//! Lists all the databases available on the server.
//! If glob is specified, lists only those databases matching it.
array(string) list_dbs (void|string glob) {
	array name,ret=({});
	object res=
		big_query(
				"SELECT datname from pg_database"+
				((glob&&sizeof(glob))? " WHERE datname ~ '"+glob_to_regexp(glob)+"'" : "")
				);
	while (name=res->fetch_row()) {
		ret += ({name[0]});
	}
	return sort(ret);
}

//! Returns an array containing the names of all the tables in the currently
//! selected database.
//! If a glob is specified, it will return only those tables
//! whose name matches it.
array(string) list_tables (void|string glob) {
	array name,ret=({});
	object res;
	res=big_query(
			"SELECT relname, relkind FROM pg_class, pg_user "
			"WHERE ( relkind = 'r' OR relkind = 'i' OR relkind = 'S') "
			"AND relname !~ '^pg_' "
			"AND usesysid = relowner " +
			((glob && sizeof(glob)) ? "AND relname ~ '"+glob_to_regexp(glob)+"' " : "") +
			"ORDER BY relname"
			);
	while (name=res->fetch_row()) {
		ret += ({name[0]});
	}
	return ret;
}

//! Returns a mapping, indexed on the column name, of mappings describing
//! the attributes of a table of the current database.
//! If a glob is specified, will return descriptions only of the columns
//! matching it.
//!
//! The currently defined fields are:
//!
//! @mapping
//!   @member int "has_rules"
//!
//!   @member int "is_shared"
//!
//!   @member string "owner"
//!     The textual representation of a Postgres uid.
//!
//!   @member string "length"
//!
//!   @member string "text"
//!     A textual description of the internal (to the server) type-name
//!
//!   @member mixed "default"
//!
//!   @member string "expires"
//!     The "relexpires" attribute for the table. Obsolescent; modern
//!     versions of Postgres don't seem to use this feature, so don't
//!     count on this field to contain any useful value.
//!
//! @endmapping
//!
array(mapping(string:mixed)) list_fields (string table, void|string wild)
{
  array row, ret=({});
  string schema;

  if (has_relexpires == "unknown")
  {
    if (catch (big_query("SELECT relexpires FROM pg_class WHERE 1 = 0")))
      has_relexpires = "no";
    else
      has_relexpires = "yes";
  }

  sscanf(table, "%s.%s", schema, table);

  object res = big_query(
	"SELECT a.attnum, a.attname, t.typname, a.attlen, c.relowner, "
	"c.relisshared, c.relhasrules, t.typdefault " +
        (has_relexpires == "yes" ? ", c.relexpires " : "") +
	(schema ? ", s.schemaname " : "") +
	"FROM pg_class c, pg_attribute a, pg_type t " +
	(schema ? ", pg_tables s " : "") +
	"WHERE c.relname = '"+table+"' AND a.attnum > 0 " +
	(schema ? "AND s.tablename = '"+table+"' " : "") +
	"AND a.attrelid = c.oid AND a.atttypid = t.oid ORDER BY attnum");

  while (row = res->fetch_row())
  {
    if (wild && sizeof(wild) && !glob(wild, row[1]))
      continue;
    ret +=
      ({ ([
	 "name":	row[1],
         "type":	row[2],
         "length":	row[3],
	 "owner":	row[4],
         "is_shared":	mkbool(row[5]),
	 "has_rules":   mkbool(row[6]),
	 "default":     row[7],
         "expires":     (sizeof(row) > 8 ? row[8] : 0)
       ]) });
  }
  return ret;
}

//! This is the only provided interface which allows you to query the
//! database. If you wish to use the simpler "query" function, you need to
//! use the @[Sql.Sql] generic SQL-object.
//!
//! It returns a postgres_result object (which conforms to the
//! @[Sql.sql_result] standard interface for accessing data). I
//! recommend using @[query()] for simpler queries (because it is
//! easier to handle, but stores all the result in memory), and
//! @[big_query()] for queries you expect to return huge amounts of
//! data (it's harder to handle, but fetches results on demand).
//!
//! @note
//! This function @b{can@} raise exceptions.
//!
//! @seealso
//!   @[Sql.Sql], @[Sql.sql_result]
int|object big_query(object|string q, mapping(string|int:mixed)|void bindings)
{  
  if(stringp(q) && String.width(q)>8)
    q=string_to_utf8(q);
  if (!bindings)
    return ::big_query(q);
  int pi=0,rep=0;
  array(string|int) paramValues=allocate(sizeof(bindings));
  array(string) from=allocate(sizeof(bindings));
  array(string) to=allocate(sizeof(bindings));
  foreach(bindings; mixed name; mixed value) {
    // Throws if mapping key is empty string.
    if(stringp(name)) {
      if(name[0]!=':')
        name=":"+name;
      if(name[1]=='_') {
        // Special parameter
        continue;
      }
    }
    from[rep]=name;
    string rval;
    if(multisetp(value)) {
      rval=sizeof(value) ? indices(value)[0] : "";
    }
    else {
      if(zero_type(value))
        paramValues[pi++]=UNDEFINED;
      else {
        if(stringp(value) && String.width(value)>8)
          value=string_to_utf8(value);
        paramValues[pi++]=(string)value;
      }
      rval="$"+(string)pi;
    }
    to[rep++]=rval;
  }
  paramValues= pi ? paramValues[..pi-1] : UNDEFINED;
  if(rep--) {
    q=replace(q,from[..rep],to[..rep]);
  }
  return ::big_query(q, paramValues);
}

#else
constant this_program_does_not_exist=1;
#endif /* constant(Postgres.postgres) */