/*
 * tdbcTokenize.c --
 *
 *	Code for a Tcl command that will extract subsitutable parameters
 *	from a SQL statement.
 *
 * Copyright (c) 2007 by D. Richard Hipp.
 * Copyright (c) 2010, 2011 by Kevin B. Kenny.
 *
 * Please refer to the file, 'license.terms' for the conditions on
 * redistribution of this file and for a DISCLAIMER OF ALL WARRANTIES.
 *
 * RCS: @(#) $Id$
 *
 *-----------------------------------------------------------------------------
 */

#include "tdbcInt.h"
#include <ctype.h>

/*
 *-----------------------------------------------------------------------------
 *
 * Tdbc_TokenizeSql --
 *
 *	Tokenizes a SQL statement.
 *
 * Results:
 *	Returns a zero-reference Tcl object that gives the statement in
 *	tokenized form, or NULL if an error occurs.
 *
 * Side effects:
 *	If an error occurs, and 'interp' is not NULL, stores an error
 *	message in the interpreter result.
 *
 * This is demonstration code for a TCL command that will extract
 * host parameters from an SQL statement.
 *
 * A "host parameter" is a variable within the SQL statement.
 * Different systems do host parameters in different ways.  This
 * tokenizer recognizes three forms:
 *
 *            $ident
 *            :ident
 *            @ident
 *
 * In other words, a host parameter is an identifier proceeded
 * by one of the '$', ':', or '@' characters.
 *
 * This function returns a Tcl_Obj representing a list.  The
 * concatenation of the returned list will be equivalent to the
 * input string.  Each element of the list will be either a
 * host parameter, a semicolon, or other text from the SQL
 * statement.
 *
 * Example:
 *
 *      tokenize_sql {SELECT * FROM table1 WHERE :name='bob';}
 *
 * Resulting in:
 *
 *      {SELECT * FROM table1 WHERE } {:name} {=} {'bob'} {;}
 *
 * The tokenizer knows about SQL comments and strings and will
 * not mistake a host parameter or semicolon embedded in a string
 * or comment as a real host parameter or semicolon.
 *
 *-----------------------------------------------------------------------------
 */

TDBCAPI Tcl_Obj*
Tdbc_TokenizeSql(
    Tcl_Interp *interp,
    const char* zSql)
{
    Tcl_Obj *resultPtr;
    Tcl_Size i;

    resultPtr = Tcl_NewObj();
    for(i = 0; zSql[i]; i++){
	switch( zSql[i] ){

	    /* Skip over quoted strings.  Strings can be quoted in several
	    ** ways:    '...'   "..."   [....]
	    */
	    case '\'':
	    case '"':
	    case '[': {
		int endChar = zSql[i];
		if (endChar == '[') endChar = ']';
		for(i++; zSql[i] && zSql[i]!=endChar; i++){}
		if (zSql[i] == 0) i--;
		break;
	    }

	    /* Skip over SQL-style comments: -- to end of line
	    */
	    case '-': {
		if (zSql[i+1] == '-') {
		     for(i+=2; zSql[i] && zSql[i]!='\n'; i++){}
		     if (zSql[i] == 0) i--;
		}
		break;
	    }

	    /* Skip over C-style comments
	    */
	    case '/': {
		if (zSql[i+1] == '*') {
		     i += 3;
		     while (zSql[i] && (zSql[i]!='/' || zSql[i-1]!='*')) {
			 i++;
		     }
		     if (zSql[i] == 0) i--;
		}
		break;
	    }

	    /* Break up multiple SQL statements at each semicolon */
	    case ';': {
		if (i>0 ){
		    Tcl_ListObjAppendElement(interp, resultPtr,
			      Tcl_NewStringObj(zSql, i));
		}
		Tcl_ListObjAppendElement(interp, resultPtr,
			  Tcl_NewStringObj(";",1));
		zSql += i + 1;
		i = -1;
		break;
	    }

	    /* Any of the characters ':', '$', or '@' which is followed
	    ** by an alphanumeric or '_' and is not preceded by the same
	    ** is a host parameter. A name following a doubled colon '::'
	    ** is also not a host parameter.
	    */
	    case ':': {
		if (i > 0 && zSql[i-1] == ':') break;
	    }
		/* fallthru */

	    case '$':
	    case '@': {
		if (i>0 && (isalnum((unsigned char)(zSql[i-1]))
			    || zSql[i-1]=='_')) break;
		if (!isalnum((unsigned char)(zSql[i+1]))
		    && zSql[i+1]!='_') break;
		if (i>0 ){
		    Tcl_ListObjAppendElement(interp, resultPtr,
			      Tcl_NewStringObj(zSql, i));
		    zSql += i;
		}
		i = 1;
		while (zSql[i] && (isalnum((unsigned char)(zSql[i]))
				   || zSql[i]=='_')) {
		    i++;
		}
		Tcl_ListObjAppendElement(interp, resultPtr,
			  Tcl_NewStringObj(zSql, i));
		zSql += i;
		i = -1;
		break;
	    }
	}
    }
    if (i > 0) {
	Tcl_ListObjAppendElement(interp, resultPtr, Tcl_NewStringObj(zSql, i));
    }
    return resultPtr;
}

/*
 *-----------------------------------------------------------------------------
 *
 * TdbcTokenizeObjCmd --
 *
 *	Tcl command to tokenize a SQL statement.
 *
 * Usage:
 *	::tdbc::tokenize statement
 *
 * Results:
 *	Returns a list as from passing the given statement to
 *	Tdbc_TokenizeSql above.
 *
 *-----------------------------------------------------------------------------
 */

MODULE_SCOPE int
TdbcTokenizeObjCmd(
    TCL_UNUSED(void *),	/* Unused */
    Tcl_Interp* interp,		/* Tcl interpreter */
    Tcl_Size objc,			/* Parameter count */
    Tcl_Obj *const *objv)	/* Parameter vector */
{
    Tcl_Obj* retval;

    /* Check param count */

    if (objc != 2) {
	Tcl_WrongNumArgs(interp, 1, objv, "statement");
	return TCL_ERROR;
    }

    /* Parse the statement */

    retval = Tdbc_TokenizeSql(interp, Tcl_GetString(objv[1]));
    if (retval == NULL) {
	return TCL_ERROR;
    }

    /* Return the tokenized statement */

    Tcl_SetObjResult(interp, retval);
    return TCL_OK;

}
