/*
 * $Id: sql.pike,v 1.27 1998/07/17 20:47:40 mast Exp $
 *
 * Implements the generic parts of the SQL-interface
 *
 * Henrik Grubbstr�m 1996-01-09
 */

//.
//. File:	sql.pike
//. RCSID:	$Id: sql.pike,v 1.27 1998/07/17 20:47:40 mast Exp $
//. Author:	Henrik Grubbstr�m (grubba@idonex.se)
//.
//. Synopsis:	Implements the generic parts of the SQL-interface.
//.
//. +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//.
//. Implements those functions that need not be present in all SQL-modules.
//.

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

//. + master_sql
//.   Object to use for the actual SQL-queries.
object master_sql;

//. + case_convert
//.   Convert all field names in mappings to lower_case.
//.   Only relevant to databases which only implement big_query(),
//.   and use upper/mixed-case fieldnames (eg Oracle).
//.   0 - No (default)
//.   1 - Yes
int case_convert;

//. - quote
//.   Quote a string so that it can safely be put in a query.
//. > s - String to quote.
function(string:string) quote = lambda (string s)
{
  // This lambda is overridden from master_sql in create().
  return(replace(s, "\'", "\'\'"));
};

//. - encode_time
//.   Converts a system time value to an appropriately formatted time
//.   spec for the database.
//. > arg 1 - Time to encode.
//. > arg 2 - If nonzero then time is taken as a "full" unix time spec
//.   (where the date part is ignored), otherwise it's converted as a
//.   seconds-since-midnight value.
function(int,void|int:string) encode_time;

//. - decode_time
//.   Converts a database time spec to a system time value.
//. > arg 1 - Time spec to decode.
//. > arg 2 - Take the date part from this system time value. If zero, a
//.   seconds-since-midnight value is returned.
function(string,void|int:int) decode_time;

//. - encode_date
//.   Converts a system time value to an appropriately formatted
//.   date-only spec for the database.
//. > arg 1 - Time to encode.
function(int:string) encode_date;

//. - decode_date
//.   Converts a database date-only spec to a system time value.
//. > arg 1 - Date spec to decode.
function(string:int) decode_date;

//. - encode_datetime
//.   Converts a system time value to an appropriately formatted
//.   date and time spec for the database.
//. > arg 1 - Time to encode.
function(int:string) encode_datetime;

//. - decode_datetime
//.   Converts a database date and time spec to a system time value.
//. > arg 1 - Date and time spec to decode.
function(string:int) decode_datetime;

//. - create
//.   Create a new generic SQL object.
//. > host
//.   object - Use this object to access the SQL-database.
//.   string - Connect to the server specified.
//.            The string should be on the format:
//.              [dbtype://][user[:password]@]hostname[:port][/database]
//.            If dbtype isn't specified, use any available database server
//.            on the specified host.
//.            If the hostname is "", access through a UNIX-domain socket or
//.            similar.
//.   zero   - Access through a UNIX-domain socket or similar.
//. > database
//.   Select this database.
//. > user
//.   User name to access the database as.
//. > password
//.   Password to access the database.
void create(void|string|object host, void|string db,
	    void|string user, void|string password)
{
  if (objectp(host)) {
    master_sql = host;
    if ((user && user != "") || (password && password != "")) {
      throw_error("Sql.sql(): Only the database argument is supported when "
		  "first argument is an object\n");
    }
    if (db && db != "") {
      master_sql->select_db(db);
    }
  }
  else {
    if (db == "") {
      db = 0;
    }
    if (user == "") {
      user = 0;
    }
    if (password == "") {
      password = 0;
    }

    array(string) program_names;

    if (host && (host != replace(host, ({ ":", "/", "@" }), ({ "", "", "" })))) {

      // The hostname is on the format:
      //
      // [dbtype://][user[:password]@]hostname[:port][/database]

      array(string) arr = host/"://";
      if ((sizeof(arr) > 1) && (arr[0] != "")) {
	if (sizeof(arr[0]/".pike") > 1) {
	  program_names = ({ arr[0] });
	} else {
	  program_names = ({ arr[0] + ".pike" });
	}
	host = arr[1..] * "://";
      }
      arr = host/"@";
      if (sizeof(arr) > 1) {
	// User and/or password specified
	host = arr[-1];
	arr = (arr[0..sizeof(arr)-2]*"@")/":";
	if (!user && sizeof(arr[0])) {
	  user = arr[0];
	}
	if (!password && (sizeof(arr) > 1)) {
	  password = arr[1..]*":";
	  if (password == "") {
	    password = 0;
	  }
	}
      }
      arr = host/"/";
      if (sizeof(arr) > 1) {
	if (!db) {
	  db = arr[-1];
	  host = arr[..sizeof(arr)-2]*"/";
	}
      }
    }

    if (host == "") {
      host = 0;
    }

    foreach(program_names || get_dir(Sql->dirname), string program_name) {
      if ((sizeof(program_name / "_result") == 1) &&
	  (sizeof(program_name / ".pike") > 1) &&
	  (program_name[..7] != "sql.pike")) {
	/* Don't call ourselves... */
	array(mixed) err;
      
	err = catch {
	  program p;
#ifdef PIKE_SQL_DEBUG
	  err = catch {p = Sql[program_name];};
#else /* !PIKE_SQL_DEBUG */
	  // Ignore compiler errors for the various sql-modules,
	  // since we might not have some.
	  // This is NOT a nice way to do it, but...
	  mixed old_inhib = master()->inhibit_compile_errors;
	  master()->inhibit_compile_errors = lambda(){};
	  err = catch {p = Sql[program_name];};
	  // Restore compiler errors mode to whatever it was before.
	  master()->inhibit_compile_errors = old_inhib;
#endif /* PIKE_SQL_DEBUG */
	  if (err) {
#ifdef PIKE_SQL_DEBUG
	    Stdio.stderr->write(sprintf("Sql.sql(): Failed to compile module Sql.%s (%s)\n",
					program_name, err[0]));
#endif /* PIKE_SQL_DEBUG */
	    if (program_names) {
	      throw(err);
	    } else {
	      throw(0);
	    }
	  }

	  if (p) {
	    err = catch {
	      if (password) {
		master_sql = p(host||"", db||"", user||"", password);
	      } else if (user) {
		master_sql = p(host||"", db||"", user);
	      } else if (db) {
		master_sql = p(host||"", db);
	      } else if (host) {
		master_sql = p(host);
	      } else {
		master_sql = p();
	      }
	      break;
	    };
	    if (err) {
	      if (program_names) {
		throw(err);
	      }
#ifdef PIKE_SQL_DEBUG
	      Stdio.stderr->write(sprintf("Sql.sql(): Failed to connect using module Sql.%s (%s)\n",
					  program_name, err[0]));
#endif /* PIKE_SQL_DEBUG */
	    }
	  } else {
	    if (program_names) {
	      throw(({ sprintf("Sql.sql(): Failed to index module Sql.%s\n",
			       program_name), backtrace() }));
	    }
#ifdef PIKE_SQL_DEBUG
	    Stdio.stderr->write(sprintf("Sql.sql(): Failed to index module Sql.%s\n",
					program_name));
#endif /* PIKE_SQL_DEBUG */
	  }
	};
	if (err && program_names) {
	  throw(err);
	}
      }
    }

    if (!master_sql)
      if (!program_names) {
	throw_error("Sql.sql(): Couldn't connect using any of the databases\n");
      } else {
	throw_error("Sql.sql(): Couldn't connect using the " +
		    (program_names[0]/".pike")[0] + " database\n");
      }
  }

  function fallback =
    lambda () {throw_error ("Function not supported in this database.");};
  if (master_sql->quote) quote = master_sql->quote;
  encode_time = master_sql->encode_time || fallback;
  decode_time = master_sql->decode_time || fallback;
  encode_date = master_sql->encode_date || fallback;
  decode_date = master_sql->decode_date || fallback;
  encode_datetime = master_sql->encode_datetime || fallback;
  decode_datetime = master_sql->decode_datetime || fallback;
}

static private array(mapping(string:mixed)) res_obj_to_array(object res_obj)
{
  if (res_obj) {
    /* Not very efficient, but sufficient */
    array(mapping(string:mixed)) res = ({});
    array(string) fieldnames;
    array(mixed) row;
      
    fieldnames = Array.map(res_obj->fetch_fields(),
			   lambda (mapping(string:mixed) m) {
      if (case_convert) {
	return(lower_case(m->name));	/* Hope this is even more unique */
      } else {
	return(m->name);		/* Hope this is unique */
      }
    } );

    while (row = res_obj->fetch_row()) {
      res += ({ mkmapping(fieldnames, row) });
    }
    return(res);
  } else {
    return(0);
  }
}

//. - error
//.   Return last error message.  
int|string error()
{
  return(master_sql->error());
}

//. - select_db
//.   Select database to access.
void select_db(string db)
{
  master_sql->select_db(db);
}

//. - compile_query
//.   Compiles the query (if possible). Otherwise returns it as is.
//.   The resulting object can be used multiple times in query() and
//.   big_query().
//. > q
//.   SQL-query to compile.
string|object compile_query(string q)
{
  if (functionp(master_sql->compile_query)) {
    return(master_sql->compile_query(q));
  }
  return(q);
}

//. - query
//.   Send an SQL query to the underlying SQL-server. The result is returned
//.   as an array of mappings indexed on the name of the columns.
//.   Returns 0 if the query didn't return any result (e.g. INSERT or similar).
//. > q
//.   Query to send to the SQL-server. This can either be a string with the
//.   query, or a previously compiled query (see compile_query()).
//. > bindings
//.   An optional mapping containing bindings of variables used in the query.
//.   A variable is identified by a colon (:) followed by a name or number.
//.   Each index in the mapping corresponds to one such variable, and the
//.   value for that index is substituted into the query wherever the variable
//.   is used.  Binary values (BLOBs) may need to be placed in multisets.
array(mapping(string:mixed)) query(object|string q,
				   mapping(string|int:mixed)|void bindings)
{
  if (functionp(master_sql->query)) {
    if (bindings) {
      return(master_sql->query(q, bindings));
    } else {
      return(master_sql->query(q));
    }
  }
  if (bindings) {
    return(res_obj_to_array(master_sql->big_query(q, bindings)));
  } else {
    return(res_obj_to_array(master_sql->big_query(q)));
  }
}

//. - big_query
//.   Send an SQL query to the underlying SQL-server. The result is returned
//.   as a Sql.sql_result object. This allows for having results larger than
//.   the available memory, and returning some more info about the result.
//.   Returns 0 if the query didn't return any result (e.g. INSERT or similar).
//. > q
//.   Query to send to the SQL-server. This can either be a string with the
//.   query, or a previously compiled query (see compile_query()).
//. > bindings
//.   An optional mapping containing bindings of variables used in the query.
//.   A variable is identified by a colon (:) followed by a name or number.
//.   Each index in the mapping corresponds to one such variable, and the
//.   value for that index is substituted into the query wherever the variable
//.   is used.  Binary values (BLOBs) may need to be placed in multisets.
object big_query(object|string q, mapping(string|int:mixed)|void bindings)
{
  object|array(mapping) pre_res;

  if (functionp(master_sql->big_query)) {
    if (bindings) {
      pre_res = master_sql->big_query(q, bindings);
    } else {
      pre_res = master_sql->big_query(q);
    }
  } else if (bindings) {
    pre_res = master_sql->query(q, bindings);
  } else {
    pre_res = master_sql->query(q);
  }
  return(pre_res && Sql.sql_result(pre_res));
}

//. - create_db
//.   Create a new database.
//. > db
//.   Name of database to create.
void create_db(string db)
{
  master_sql->create_db(db);
}

//. - drop_db
//.   Drop database
//. > db
//.   Name of database to drop.
void drop_db(string db)
{
  master_sql->drop_db(db);
}

//. - shutdown
//.   Shutdown a database server.
void shutdown()
{
  if (functionp(master_sql->shutdown)) {
    master_sql->shutdown();
  } else {
    throw_error("sql->shutdown(): Not supported by this database\n");
  }
}

//. - reload
//.   Reload the tables.
void reload()
{
  if (functionp(master_sql->reload)) {
    master_sql->reload();
  } else {
    /* Probably safe to make this a NOOP */
  }
}

//. - server_info
//.   Return info about the current SQL-server.
string server_info()
{
  if (functionp(master_sql->server_info)) {
    return(master_sql->server_info());
  }
  return("Unknown SQL-server");
}

//. - host_info
//.   Return info about the connection to the SQL-server.
string host_info()
{
  if (functionp(master_sql->host_info)) {
    return(master_sql->host_info());
  } 
  return("Unknown connection to host");
}

//. - list_dbs
//.   List available databases on this SQL-server.
//. > wild
//.   Optional wildcard to match against.
array(string) list_dbs(string|void wild)
{
  array(string)|array(mapping(string:mixed))|object res;
  
  if (functionp(master_sql->list_dbs)) {
    if (objectp(res = master_sql->list_dbs())) {
      res = res_obj_to_array(res);
    }
  } else {
    res = query("show databases");
  }
  if (sizeof(res) && mappingp(res[0])) {
    res = Array.map(res, lambda (mapping m) {
      return(values(m)[0]);	/* Hope that there's only one field */
    } );
  }
  if (wild) {
    res = Simulate.map_regexp(res,
			      replace(wild, ({ "%", "_" }), ({ ".*", "." }) ));
  }
  return(res);
}

//. - list_tables
//.   List tables available in the current database.
//. > wild
//.   Optional wildcard to match against.
array(string) list_tables(string|void wild)
{
  array(string)|array(mapping(string:mixed))|object res;
  
  if (functionp(master_sql->list_tables)) {
    if (objectp(res = master_sql->list_tables())) {
      res = res_obj_to_array(res);
    }
  } else {
    res = query("show tables");
  }
  if (sizeof(res) && mappingp(res[0])) {
    res = Array.map(res, lambda (mapping m) {
      return(values(m)[0]);	/* Hope that there's only one field */
    } );
  }
  if (wild) {
    res = Simulate.map_regexp(res,
			      replace(wild, ({ "%", "_" }), ({ ".*", "." }) ));
  }
  return(res);
}

//. - list_fields
//.   List fields available in the specified table
//. > table
//.   Table to list the fields of.
//. > wild
//.   Optional wildcard to match against.
array(mapping(string:mixed)) list_fields(string table, string|void wild)
{
  array(mapping(string:mixed))|object res;

  if (functionp(master_sql->list_fields)) {
    if (objectp(res = master_sql->list_fields(table))) {
      res = res_obj_to_array(res);
    }
    if (wild) {
      /* Not very efficient, but... */
      res = Array.filter(res, lambda (mapping m, string re) {
	return(sizeof(Simulate.map_regexp( ({ m->name }), re)));
      }, replace(wild, ({ "%", "_" }), ({ ".*", "." }) ) );
    }
    return(res);
  }
  if (wild) {
    res = query("show fields from \'" + table +
		"\' like \'" + wild + "\'");
  } else {
    res = query("show fields from \'" + table + "\'");
  }
  res = Array.map(res, lambda (mapping m, string table) {
    foreach(indices(m), string str) {
      /* Add the lower case variants */
      string low_str = lower_case(str);
      if (low_str != str && !m[low_str]) {
	m[low_str] = m[str];
	m_delete(m, str);	/* Remove duplicate */
      }
    }
    if ((!m->name) && m->field) {
      m["name"] = m->field;
      m_delete(m, "field");	/* Remove duplicate */
    }
    if (!m->table) {
      m["table"] = table;
    }
    return(m);
  }, table);
  return(res);
}