// clang-format off
/*
 *	$Id: sql.c 761 2010-06-04 12:40:28Z dfishburn $
 *
 *	Copyright (c) 2002-2003, Darren Hiebert
 *
 *	This source code is released for free distribution under the terms of the
 *	GNU General Public License.
 *
 *	This module contains functions for generating tags for PL/SQL language
 *	files.
 */

/*
 *	 INCLUDE FILES
 */
#include "third_party/ctags/general.h"	/* must always come first */

#include "libc/str/str.h"	/* to define isalpha () */
#include "libc/runtime/runtime.h"
#ifdef DEBUG
#include "libc/calls/calls.h"
#include "libc/calls/dprintf.h"
#include "libc/calls/weirdtypes.h"
#include "libc/fmt/fmt.h"
#include "libc/mem/fmt.h"
#include "libc/stdio/stdio.h"
#include "libc/stdio/temp.h"
#include "third_party/musl/tempnam.h"
#endif

#include "third_party/ctags/debug.h"
#include "third_party/ctags/entry.h"
#include "third_party/ctags/keyword.h"
#include "third_party/ctags/parse.h"
#include "third_party/ctags/read.h"
#include "third_party/ctags/routines.h"
#include "third_party/ctags/vstring.h"

/*
 *	On-line "Oracle Database PL/SQL Language Reference":
 *	http://download.oracle.com/docs/cd/B28359_01/appdev.111/b28370/toc.htm
 *
 *	Sample PL/SQL code is available from:
 *	http://www.orafaq.com/faqscrpt.htm#GENPLSQL
 *
 *	On-line SQL Anywhere Documentation
 *	http://www.ianywhere.com/developer/product_manuals/sqlanywhere/index.html
 */

/*
 *	 MACROS
 */
#define isType(token,t)		(boolean) ((token)->type == (t))
#define isKeyword(token,k)	(boolean) ((token)->keyword == (k))

/*
 *	 DATA DECLARATIONS
 */

typedef enum eException { ExceptionNone, ExceptionEOF } exception_t;

/*
 * Used to specify type of keyword.
 */
typedef enum eKeywordId {
	KEYWORD_NONE = -1,
	KEYWORD_is,
	KEYWORD_begin,
	KEYWORD_body,
	KEYWORD_cursor,
	KEYWORD_declare,
	KEYWORD_end,
	KEYWORD_function,
	KEYWORD_if,
	KEYWORD_else,
	KEYWORD_elseif,
	KEYWORD_endif,
	KEYWORD_loop,
	KEYWORD_while,
	KEYWORD_case,
	KEYWORD_for,
	KEYWORD_do,
	KEYWORD_call,
	KEYWORD_package,
	KEYWORD_pragma,
	KEYWORD_procedure,
	KEYWORD_record,
	KEYWORD_object,
	KEYWORD_ref,
	KEYWORD_rem,
	KEYWORD_return,
	KEYWORD_returns,
	KEYWORD_subtype,
	KEYWORD_table,
	KEYWORD_trigger,
	KEYWORD_type,
	KEYWORD_index,
	KEYWORD_event,
	KEYWORD_publication,
	KEYWORD_service,
	KEYWORD_domain,
	KEYWORD_datatype,
	KEYWORD_result,
	KEYWORD_url,
	KEYWORD_internal,
	KEYWORD_external,
	KEYWORD_when,
	KEYWORD_then,
	KEYWORD_variable,
	KEYWORD_exception,
	KEYWORD_at,
	KEYWORD_on,
	KEYWORD_primary,
	KEYWORD_references,
	KEYWORD_unique,
	KEYWORD_check,
	KEYWORD_constraint,
	KEYWORD_foreign,
	KEYWORD_ml_table,
	KEYWORD_ml_table_lang,
	KEYWORD_ml_table_dnet,
	KEYWORD_ml_table_java,
	KEYWORD_ml_table_chk,
	KEYWORD_ml_conn,
	KEYWORD_ml_conn_lang,
	KEYWORD_ml_conn_dnet,
	KEYWORD_ml_conn_java,
	KEYWORD_ml_conn_chk,
	KEYWORD_ml_prop,
	KEYWORD_local,
	KEYWORD_temporary,
	KEYWORD_drop,
	KEYWORD_view,
	KEYWORD_synonym,
	KEYWORD_handler,
	KEYWORD_comment,
	KEYWORD_create,
	KEYWORD_go
} keywordId;

/*
 * Used to determine whether keyword is valid for the token language and
 *	what its ID is.
 */
typedef struct sKeywordDesc {
	const char *name;
	keywordId id;
} keywordDesc;

typedef enum eTokenType {
	TOKEN_UNDEFINED,
	TOKEN_BLOCK_LABEL_BEGIN,
	TOKEN_BLOCK_LABEL_END,
	TOKEN_CHARACTER,
	TOKEN_CLOSE_PAREN,
	TOKEN_COLON,
	TOKEN_SEMICOLON,
	TOKEN_COMMA,
	TOKEN_IDENTIFIER,
	TOKEN_KEYWORD,
	TOKEN_OPEN_PAREN,
	TOKEN_OPERATOR,
	TOKEN_OTHER,
	TOKEN_STRING,
	TOKEN_PERIOD,
	TOKEN_OPEN_CURLY,
	TOKEN_CLOSE_CURLY,
	TOKEN_OPEN_SQUARE,
	TOKEN_CLOSE_SQUARE,
	TOKEN_TILDE,
	TOKEN_FORWARD_SLASH,
	TOKEN_EQUAL
} tokenType;

typedef struct sTokenInfoSQL {
	tokenType	type;
	keywordId	keyword;
	vString *	string;
	vString *	scope;
	int         begin_end_nest_lvl;
	unsigned long lineNumber;
	fpos_t filePosition;
} tokenInfo;

/*
 *	DATA DEFINITIONS
 */

static langType Lang_sql;

static jmp_buf Exception;

typedef enum {
	SQLTAG_CURSOR,
	SQLTAG_PROTOTYPE,
	SQLTAG_FUNCTION,
	SQLTAG_FIELD,
	SQLTAG_LOCAL_VARIABLE,
	SQLTAG_BLOCK_LABEL,
	SQLTAG_PACKAGE,
	SQLTAG_PROCEDURE,
	SQLTAG_RECORD,
	SQLTAG_SUBTYPE,
	SQLTAG_TABLE,
	SQLTAG_TRIGGER,
	SQLTAG_VARIABLE,
	SQLTAG_INDEX,
	SQLTAG_EVENT,
	SQLTAG_PUBLICATION,
	SQLTAG_SERVICE,
	SQLTAG_DOMAIN,
	SQLTAG_VIEW,
	SQLTAG_SYNONYM,
	SQLTAG_MLTABLE,
	SQLTAG_MLCONN,
	SQLTAG_MLPROP,
	SQLTAG_COUNT
} sqlKind;

static kindOption SqlKinds [] = {
	{ TRUE,  'c', "cursor",		  "cursors"				   },
	{ FALSE, 'd', "prototype",	  "prototypes"			   },
	{ TRUE,  'f', "function",	  "functions"			   },
	{ TRUE,  'F', "field",		  "record fields"		   },
	{ FALSE, 'l', "local",		  "local variables"		   },
	{ TRUE,  'L', "label",		  "block label"			   },
	{ TRUE,  'P', "package",	  "packages"			   },
	{ TRUE,  'p', "procedure",	  "procedures"			   },
	{ FALSE, 'r', "record",		  "records"				   },
	{ TRUE,  's', "subtype",	  "subtypes"			   },
	{ TRUE,  't', "table",		  "tables"				   },
	{ TRUE,  'T', "trigger",	  "triggers"			   },
	{ TRUE,  'v', "variable",	  "variables"			   },
	{ TRUE,  'i', "index",		  "indexes"				   },
	{ TRUE,  'e', "event",		  "events"				   },
	{ TRUE,  'U', "publication",  "publications"		   },
	{ TRUE,  'R', "service",	  "services"			   },
	{ TRUE,  'D', "domain",		  "domains"				   },
	{ TRUE,  'V', "view",		  "views"				   },
	{ TRUE,  'n', "synonym",	  "synonyms"			   },
	{ TRUE,  'x', "mltable",	  "MobiLink Table Scripts" },
	{ TRUE,  'y', "mlconn",		  "MobiLink Conn Scripts"  },
	{ TRUE,  'z', "mlprop",		  "MobiLink Properties "   }
};

static const keywordDesc SqlKeywordTable [] = {
	/* keyword		keyword ID */
	{ "as",								KEYWORD_is				      },
	{ "is",								KEYWORD_is				      },
	{ "begin",							KEYWORD_begin			      },
	{ "body",							KEYWORD_body			      },
	{ "cursor",							KEYWORD_cursor			      },
	{ "declare",						KEYWORD_declare			      },
	{ "end",							KEYWORD_end				      },
	{ "function",						KEYWORD_function		      },
	{ "if",								KEYWORD_if				      },
	{ "else",							KEYWORD_else			      },
	{ "elseif",							KEYWORD_elseif			      },
	{ "endif",							KEYWORD_endif			      },
	{ "loop",							KEYWORD_loop			      },
	{ "while",							KEYWORD_while			      },
	{ "case",							KEYWORD_case			      },
	{ "for",							KEYWORD_for				      },
	{ "do",								KEYWORD_do				      },
	{ "call",							KEYWORD_call			      },
	{ "package",						KEYWORD_package			      },
	{ "pragma",							KEYWORD_pragma			      },
	{ "procedure",						KEYWORD_procedure		      },
	{ "record",							KEYWORD_record			      },
	{ "object",							KEYWORD_object			      },
	{ "ref",							KEYWORD_ref				      },
	{ "rem",							KEYWORD_rem				      },
	{ "return",							KEYWORD_return			      },
	{ "returns",						KEYWORD_returns			      },
	{ "subtype",						KEYWORD_subtype			      },
	{ "table",							KEYWORD_table			      },
	{ "trigger",						KEYWORD_trigger			      },
	{ "type",							KEYWORD_type			      },
	{ "index",							KEYWORD_index			      },
	{ "event",							KEYWORD_event			      },
	{ "publication",					KEYWORD_publication		      },
	{ "service",						KEYWORD_service			      },
	{ "domain",							KEYWORD_domain				  },
	{ "datatype",						KEYWORD_datatype		      },
	{ "result",							KEYWORD_result			      },
	{ "url",							KEYWORD_url				      },
	{ "internal",						KEYWORD_internal		      },
	{ "external",						KEYWORD_external		      },
	{ "when",							KEYWORD_when			      },
	{ "then",							KEYWORD_then			      },
	{ "variable",						KEYWORD_variable		      },
	{ "exception",						KEYWORD_exception		      },
	{ "at",								KEYWORD_at				      },
	{ "on",								KEYWORD_on				      },
	{ "primary",						KEYWORD_primary			      },
	{ "references",						KEYWORD_references		      },
	{ "unique",							KEYWORD_unique			      },
	{ "check",							KEYWORD_check			      },
	{ "constraint",						KEYWORD_constraint		      },
	{ "foreign",						KEYWORD_foreign			      },
	{ "ml_add_table_script",			KEYWORD_ml_table		      },
	{ "ml_add_lang_table_script",		KEYWORD_ml_table_lang	      },
	{ "ml_add_dnet_table_script",		KEYWORD_ml_table_dnet	      },
	{ "ml_add_java_table_script",		KEYWORD_ml_table_java	      },
	{ "ml_add_lang_table_script_chk",	KEYWORD_ml_table_chk	      },
	{ "ml_add_connection_script",		KEYWORD_ml_conn			      },
	{ "ml_add_lang_connection_script",	KEYWORD_ml_conn_lang	      },
	{ "ml_add_dnet_connection_script",	KEYWORD_ml_conn_dnet	      },
	{ "ml_add_java_connection_script",	KEYWORD_ml_conn_java	      },
	{ "ml_add_lang_conn_script_chk",	KEYWORD_ml_conn_chk 	      },
	{ "ml_add_property",				KEYWORD_ml_prop		 	      },
	{ "local",							KEYWORD_local			      },
	{ "temporary",						KEYWORD_temporary		      },
	{ "drop",							KEYWORD_drop			      },
	{ "view",							KEYWORD_view			      },
	{ "synonym",						KEYWORD_synonym			      },
	{ "handler",						KEYWORD_handler			      },
	{ "comment",						KEYWORD_comment			      },
	{ "create",							KEYWORD_create				  },
	{ "go",								KEYWORD_go				      }
};

/*
 *	 FUNCTION DECLARATIONS
 */

/* Recursive calls */
static void parseBlock (tokenInfo *const token, const boolean local);
static void parseDeclare (tokenInfo *const token, const boolean local);
static void parseKeywords (tokenInfo *const token);
static void parseSqlFile (tokenInfo *const token);

/*
 *	 FUNCTION DEFINITIONS
 */

static boolean isIdentChar1 (const int c)
{
	/*
	 * Other databases are less restrictive on the first character of
	 * an identifier.
	 * isIdentChar1 is used to identify the first character of an 
	 * identifier, so we are removing some restrictions.
	 */
	return (boolean)
		(isalpha (c) || c == '@' || c == '_' );
}

static boolean isIdentChar (const int c)
{
	return (boolean)
		(isalpha (c) || isdigit (c) || c == '$' || 
		 c == '@' || c == '_' || c == '#');
}

static boolean isCmdTerm (tokenInfo *const token)
{
	DebugStatement ( 
			debugPrintf (DEBUG_PARSE
				, "\n isCmdTerm: token same  tt:%d  tk:%d\n"
				, token->type
				, token->keyword
				);
			);

	/*
	 * Based on the various customer sites I have been at
	 * the most common command delimiters are
	 *	   ;
	 *	   ~
	 *	   /
	 *	   go
	 * This routine will check for any of these, more
	 * can easily be added by modifying readToken and
	 * either adding the character to:
	 *	   enum eTokenType
	 *	   enum eTokenType
	 */
	return ( isType (token, TOKEN_SEMICOLON) || 
			isType (token, TOKEN_TILDE) || 
			isType (token, TOKEN_FORWARD_SLASH) || 
			isKeyword (token, KEYWORD_go) 
			);
}

static boolean isMatchedEnd(tokenInfo *const token, int nest_lvl)
{
	boolean terminated = FALSE;
	/*
	 * Since different forms of SQL allow the use of 
	 * BEGIN 
	 * ...
	 * END 
	 * blocks, some statements may not be terminated using
	 * the standard delimiters:
	 *	   ;
	 *	   ~
	 *	   /
	 *	   go
	 * This routine will check to see if we encounter and END
	 * for the matching nest level of BEGIN ... END statements.
	 * If we find one, then we can assume, the statement was terminated
	 * since we have fallen through to the END statement of the BEGIN
	 * block.
	 */
	if ( nest_lvl > 0 && isKeyword (token, KEYWORD_end) )
	{
		if ( token->begin_end_nest_lvl == nest_lvl )
			terminated = TRUE;
	}

	return terminated;
}

static void buildSqlKeywordHash (void)
{
	const size_t count = sizeof (SqlKeywordTable) /
		sizeof (SqlKeywordTable [0]);
	size_t i;
	for (i = 0	;  i < count  ;  ++i)
	{
		const keywordDesc* const p = &SqlKeywordTable [i];
		addKeyword (p->name, Lang_sql, (int) p->id);
	}
}

static tokenInfo *newToken (void)
{
	tokenInfo *const token = xMalloc (1, tokenInfo);

	token->type               = TOKEN_UNDEFINED;
	token->keyword            = KEYWORD_NONE;
	token->string             = vStringNew ();
	token->scope              = vStringNew ();
	token->begin_end_nest_lvl = 0;
	token->lineNumber         = getSourceLineNumber ();
	token->filePosition       = getInputFilePosition ();

	return token;
}

static void deleteToken (tokenInfo *const token)
{
	vStringDelete (token->string);
	vStringDelete (token->scope);
	eFree (token);
}

/*
 *	 Tag generation functions
 */

static void makeConstTag (tokenInfo *const token, const sqlKind kind)
{
	if (SqlKinds [kind].enabled)
	{
		const char *const name = vStringValue (token->string);
		tagEntryInfo e;
		initTagEntry (&e, name);

		e.lineNumber   = token->lineNumber;
		e.filePosition = token->filePosition;
		e.kindName	   = SqlKinds [kind].name;
		e.kind		   = SqlKinds [kind].letter;

		makeTagEntry (&e);
	}
}

static void makeSqlTag (tokenInfo *const token, const sqlKind kind)
{
	vString *	fulltag;

	if (SqlKinds [kind].enabled)
	{
		/*
		 * If a scope has been added to the token, change the token
		 * string to include the scope when making the tag.
		 */
		if ( vStringLength(token->scope) > 0 )
		{
			fulltag = vStringNew ();
			vStringCopy(fulltag, token->scope);
			vStringCatS (fulltag, ".");
			vStringCatS (fulltag, vStringValue(token->string));
			vStringTerminate(fulltag);
			vStringCopy(token->string, fulltag);
			vStringDelete (fulltag);
		}
		makeConstTag (token, kind);
	}
}

/*
 *	 Parsing functions
 */

static void parseString (vString *const string, const int delimiter)
{
	boolean end = FALSE;
	while (! end)
	{
		int c = fileGetc ();
		if (c == EOF)
			end = TRUE;
		/*
		else if (c == '\\')
		{
			c = fileGetc(); // This maybe a ' or ". //
			vStringPut(string, c);
		}
		*/
		else if (c == delimiter)
			end = TRUE;
		else
			vStringPut (string, c);
	}
	vStringTerminate (string);
}

/*	Read a C identifier beginning with "firstChar" and places it into "name".
*/
static void parseIdentifier (vString *const string, const int firstChar)
{
	int c = firstChar;
	Assert (isIdentChar1 (c));
	do
	{
		vStringPut (string, c);
		c = fileGetc ();
	} while (isIdentChar (c));
	vStringTerminate (string);
	if (!isspace (c))
		fileUngetc (c);		/* unget non-identifier character */
}

static void readToken (tokenInfo *const token)
{
	int c;

	token->type			= TOKEN_UNDEFINED;
	token->keyword		= KEYWORD_NONE;
	vStringClear (token->string);

getNextChar:
	do
	{
		c = fileGetc ();
		token->lineNumber   = getSourceLineNumber ();
		token->filePosition = getInputFilePosition ();
		/* 
		 * Added " to the list of ignores, not sure what this 
		 * might break but it gets by this issue:
		 *	  create table "t1" (...)
		 *
		 * Darren, the code passes all my tests for both 
		 * Oracle and SQL Anywhere, but maybe you can tell me
		 * what this may effect.
		 */
	}
	while (c == '\t'  ||  c == ' ' ||  c == '\n');

	switch (c)
	{
		case EOF: longjmp (Exception, (int)ExceptionEOF);	break;
		case '(': token->type = TOKEN_OPEN_PAREN;		break;
		case ')': token->type = TOKEN_CLOSE_PAREN;		break;
		case ':': token->type = TOKEN_COLON;			break;
		case ';': token->type = TOKEN_SEMICOLON;		break;
		case '.': token->type = TOKEN_PERIOD;			break;
		case ',': token->type = TOKEN_COMMA;			break;
		case '{': token->type = TOKEN_OPEN_CURLY;		break;
		case '}': token->type = TOKEN_CLOSE_CURLY;		break;
		case '~': token->type = TOKEN_TILDE;			break;
		case '[': token->type = TOKEN_OPEN_SQUARE;		break;
		case ']': token->type = TOKEN_CLOSE_SQUARE;		break;
		case '=': token->type = TOKEN_EQUAL;			break;

		case '\'':
		case '"':
				  token->type = TOKEN_STRING;
				  parseString (token->string, c);
				  token->lineNumber = getSourceLineNumber ();
				  token->filePosition = getInputFilePosition ();
				  break;

		case '-':
				  c = fileGetc ();
				  if (c == '-')		/* -- is this the start of a comment? */
				  {
					  fileSkipToCharacter ('\n');
					  goto getNextChar;
				  }
				  else
				  {
					  if (!isspace (c))
						  fileUngetc (c);
					  token->type = TOKEN_OPERATOR;
				  }
				  break;

		case '<':
		case '>':
				  {
					  const int initial = c;
					  int d = fileGetc ();
					  if (d == initial)
					  {
						  if (initial == '<')
							  token->type = TOKEN_BLOCK_LABEL_BEGIN;
						  else
							  token->type = TOKEN_BLOCK_LABEL_END;
					  }
					  else
					  {
						  fileUngetc (d);
						  token->type = TOKEN_UNDEFINED;
					  }
					  break;
				  }

		case '\\':
				  c = fileGetc ();
				  if (c != '\\'  && c != '"'  && c != '\''  &&  !isspace (c))
					  fileUngetc (c);
				  token->type = TOKEN_CHARACTER;
				  token->lineNumber = getSourceLineNumber ();
				  token->filePosition = getInputFilePosition ();
				  break;

		case '/':
				  {
					  int d = fileGetc ();
					  if ( (d != '*') &&		/* is this the start of a comment? */
							  (d != '/') )			/* is a one line comment? */
					  {
						  token->type = TOKEN_FORWARD_SLASH;
						  fileUngetc (d);
					  }
					  else
					  {
						  if (d == '*')
						  {
							  do
							  {
								  fileSkipToCharacter ('*');
								  c = fileGetc ();
								  if (c == '/')
									  break;
								  else
									  fileUngetc (c);
							  } while (c != EOF && c != '\0');
							  goto getNextChar;
						  }
						  else if (d == '/')	/* is this the start of a comment?  */
						  {
							  fileSkipToCharacter ('\n');
							  goto getNextChar;
						  }
					  }
					  break;
				  }

		default:
				  if (! isIdentChar1 (c))
					  token->type = TOKEN_UNDEFINED;
				  else
				  {
					  parseIdentifier (token->string, c);
					  token->lineNumber = getSourceLineNumber ();
					  token->filePosition = getInputFilePosition ();
					  token->keyword = analyzeToken (token->string, Lang_sql);
					  if (isKeyword (token, KEYWORD_rem))
					  {
						  vStringClear (token->string);
						  fileSkipToCharacter ('\n');
						  goto getNextChar;
					  }
					  else if (isKeyword (token, KEYWORD_NONE))
						  token->type = TOKEN_IDENTIFIER;
					  else
						  token->type = TOKEN_KEYWORD;
				  }
				  break;
	}
}

/*
 *	 Token parsing functions
 */

/*
 * static void addContext (tokenInfo* const parent, const tokenInfo* const child)
 * {
 *	   if (vStringLength (parent->string) > 0)
 *	   {
 *		   vStringCatS (parent->string, ".");
 *	   }
 *	   vStringCatS (parent->string, vStringValue(child->string));
 *	   vStringTerminate(parent->string);
 * }
 */

static void addToScope (tokenInfo* const token, vString* const extra)
{
	if (vStringLength (token->scope) > 0)
	{
		vStringCatS (token->scope, ".");
	}
	vStringCatS (token->scope, vStringValue(extra));
	vStringTerminate(token->scope);
}

/*
 *	 Scanning functions
 */

static void findToken (tokenInfo *const token, const tokenType type)
{
	while (! isType (token, type))
	{
		readToken (token);
	}
}

static void findCmdTerm (tokenInfo *const token, const boolean check_first)
{
	int begin_end_nest_lvl = token->begin_end_nest_lvl;

	if ( check_first ) 
	{
		if ( isCmdTerm(token) )
			return;
	}
	do
	{
		readToken (token);
	} while ( !isCmdTerm(token) && !isMatchedEnd(token, begin_end_nest_lvl) );
}

static void skipToMatched(tokenInfo *const token)
{
	int nest_level = 0;
	tokenType open_token;
	tokenType close_token;

	switch (token->type)
	{
		case TOKEN_OPEN_PAREN:
			open_token  = TOKEN_OPEN_PAREN;
			close_token = TOKEN_CLOSE_PAREN;
			break;
		case TOKEN_OPEN_CURLY:
			open_token  = TOKEN_OPEN_CURLY;
			close_token = TOKEN_CLOSE_CURLY;
			break;
		case TOKEN_OPEN_SQUARE:
			open_token  = TOKEN_OPEN_SQUARE;
			close_token = TOKEN_CLOSE_SQUARE;
			break;
		default:
			return;
	}

	/*
	 * This routine will skip to a matching closing token.
	 * It will also handle nested tokens like the (, ) below.
	 *	 (	name varchar(30), text binary(10)  )
	 */

	if (isType (token, open_token))	
	{
		nest_level++;
		while (! (isType (token, close_token) && (nest_level == 0)))
		{
			readToken (token);
			if (isType (token, open_token))
			{
				nest_level++;
			}
			if (isType (token, close_token))
			{
				if (nest_level > 0)
				{
					nest_level--;
				}
			}
		} 
		readToken (token);
	}
}

static void copyToken (tokenInfo *const dest, tokenInfo *const src)
{
	dest->lineNumber = src->lineNumber;
	dest->filePosition = src->filePosition;
	dest->type = src->type;
	dest->keyword = src->keyword;
	vStringCopy(dest->string, src->string);
	vStringCopy(dest->scope, src->scope);
}

static void skipArgumentList (tokenInfo *const token)
{
	/*
	 * Other databases can have arguments with fully declared
	 * datatypes:
	 *	 (	name varchar(30), text binary(10)  )
	 * So we must check for nested open and closing parantheses
	 */

	if (isType (token, TOKEN_OPEN_PAREN))	/* arguments? */
	{
		skipToMatched (token);
	}
}

static void parseSubProgram (tokenInfo *const token)
{
	tokenInfo *const name  = newToken ();
	vString * saveScope = vStringNew ();

	/*
	 * This must handle both prototypes and the body of
	 * the procedures.
	 *
	 * Prototype:
	 *	   FUNCTION func_name RETURN integer;
	 *	   PROCEDURE proc_name( parameters );
	 * Procedure
	 *	   FUNCTION GET_ML_USERNAME RETURN VARCHAR2
	 *	   IS
	 *	   BEGIN
	 *		   RETURN v_sync_user_id;
	 *	   END GET_ML_USERNAME;
	 *
	 *	   PROCEDURE proc_name( parameters )
	 *		   IS
	 *		   BEGIN
	 *		   END;
	 *	   CREATE PROCEDURE proc_name( parameters )
	 *		   EXTERNAL NAME ... ;
	 *	   CREATE PROCEDURE proc_name( parameters )
	 *		   BEGIN
	 *		   END;
	 *
	 *	   CREATE FUNCTION f_GetClassName(
	 *		   IN @object VARCHAR(128)
	 *		  ,IN @code   VARCHAR(128)
	 *	   )
	 *	   RETURNS VARCHAR(200)
	 *	   DETERMINISTIC
	 *	   BEGIN
	 *	   
	 *		   IF( @object = 'user_state' ) THEN
	 *			   SET something = something;
	 *		   END IF;
	 *	   
	 *		   RETURN @name;
	 *	   END;
	 *
	 * Note, a Package adds scope to the items within.
     *     create or replace package demo_pkg is
     *         test_var number;
     *         function test_func return varchar2;
     *         function more.test_func2 return varchar2;
     *     end demo_pkg;
	 * So the tags generated here, contain the package name:
     *         demo_pkg.test_var
     *         demo_pkg.test_func
     *         demo_pkg.more.test_func2
	 */
	const sqlKind kind = isKeyword (token, KEYWORD_function) ?
		SQLTAG_FUNCTION : SQLTAG_PROCEDURE;
	Assert (isKeyword (token, KEYWORD_function) ||
			isKeyword (token, KEYWORD_procedure));

	vStringCopy(saveScope, token->scope);
	readToken (token);
	copyToken (name, token);
	readToken (token);

	if (isType (token, TOKEN_PERIOD))
	{
		/*
		 * If this is an Oracle package, then the token->scope should
		 * already be set.  If this is the case, also add this value to the
		 * scope.
		 * If this is not an Oracle package, chances are the scope should be
		 * blank and the value just read is the OWNER or CREATOR of the
		 * function and should not be considered part of the scope.
		 */
		if ( vStringLength(saveScope) > 0 )
		{
			addToScope(token, name->string);
		}
		readToken (token);
		copyToken (name, token);
		readToken (token);
	}
	if (isType (token, TOKEN_OPEN_PAREN))
	{
		/* Reads to the next token after the TOKEN_CLOSE_PAREN */
		skipArgumentList(token);
	}

	if (kind == SQLTAG_FUNCTION)
	{
		if (isKeyword (token, KEYWORD_return) || isKeyword (token, KEYWORD_returns))
		{
			/* Read datatype */
			readToken (token);
			/*
			 * Read token after which could be the
			 * command terminator if a prototype
			 * or an open parantheses
			 */
			readToken (token);
			if (isType (token, TOKEN_OPEN_PAREN))
			{
				/* Reads to the next token after the TOKEN_CLOSE_PAREN */
				skipArgumentList(token);
			}
		}
	}
	if( isCmdTerm (token) )
	{
		makeSqlTag (name, SQLTAG_PROTOTYPE);
	} 
	else 
	{
		while (!(isKeyword (token, KEYWORD_is) ||
					isKeyword (token, KEYWORD_begin) ||
					isKeyword (token, KEYWORD_at) ||
					isKeyword (token, KEYWORD_internal) ||
					isKeyword (token, KEYWORD_external) ||
					isKeyword (token, KEYWORD_url) ||
					isType (token, TOKEN_EQUAL) ||
					isCmdTerm (token)
				)
			  )
		{
			if ( isKeyword (token, KEYWORD_result) )
			{
				readToken (token);
				if (isType (token, TOKEN_OPEN_PAREN))
				{
					/* Reads to the next token after the TOKEN_CLOSE_PAREN */
					skipArgumentList(token);
				}
			} else {
				readToken (token);
			}
		}
		if (isKeyword (token, KEYWORD_at) || 
				isKeyword (token, KEYWORD_url) ||
				isKeyword (token, KEYWORD_internal) ||
				isKeyword (token, KEYWORD_external) )
		{
			addToScope(token, name->string);
			if (isType (name, TOKEN_IDENTIFIER) ||
					isType (name, TOKEN_STRING) ||
					!isKeyword (token, KEYWORD_NONE)
			   )
				makeSqlTag (name, kind);

			vStringClear (token->scope);
		} 
		if ( isType (token, TOKEN_EQUAL) )
			readToken (token);

		if ( isKeyword (token, KEYWORD_declare) )
			parseDeclare (token, FALSE);

		if (isKeyword (token, KEYWORD_is) || 
				isKeyword (token, KEYWORD_begin) )
		{
			addToScope(token, name->string);
			if (isType (name, TOKEN_IDENTIFIER) ||
					isType (name, TOKEN_STRING) ||
					!isKeyword (token, KEYWORD_NONE)
			   )
				makeSqlTag (name, kind);

			parseBlock (token, TRUE);
			vStringClear (token->scope);
		} 
	}
	vStringCopy(token->scope, saveScope);
	deleteToken (name);
	vStringDelete(saveScope);
}

static void parseRecord (tokenInfo *const token)
{
	/*
	 * Make it a bit forgiving, this is called from
	 * multiple functions, parseTable, parseType
	 */
	if (!isType (token, TOKEN_OPEN_PAREN))
		readToken (token);

	Assert (isType (token, TOKEN_OPEN_PAREN));
	do
	{
		if ( isType (token, TOKEN_COMMA) || isType (token, TOKEN_OPEN_PAREN) )
			readToken (token);

		/*
		 * Create table statements can end with various constraints
		 * which must be excluded from the SQLTAG_FIELD.
		 *	  create table t1 (
		 *		  c1 integer,
		 *		  c2 char(30),
		 *		  c3 numeric(10,5),
		 *		  c4 integer,
		 *		  constraint whatever,
		 *		  primary key(c1),
		 *		  foreign key (), 
		 *		  check ()
		 *	  )
		 */
		if (! (isKeyword(token, KEYWORD_primary) ||
					isKeyword(token, KEYWORD_references) ||
					isKeyword(token, KEYWORD_unique) ||
					isKeyword(token, KEYWORD_check) ||
					isKeyword(token, KEYWORD_constraint) ||
					isKeyword(token, KEYWORD_foreign) ) )
		{
			if (isType (token, TOKEN_IDENTIFIER) ||
					isType (token, TOKEN_STRING))
				makeSqlTag (token, SQLTAG_FIELD);
		}

		while (!(isType (token, TOKEN_COMMA) ||
					isType (token, TOKEN_CLOSE_PAREN) ||
					isType (token, TOKEN_OPEN_PAREN) 
				))
		{
			readToken (token);
			/* 
			 * A table structure can look like this:
			 *	  create table t1 (
			 *		  c1 integer,
			 *		  c2 char(30),
			 *		  c3 numeric(10,5),
			 *		  c4 integer
			 *	  )
			 * We can't just look for a COMMA or CLOSE_PAREN
			 * since that will not deal with the numeric(10,5)
			 * case.  So we need to skip the argument list 
			 * when we find an open paren.
			 */
			if (isType (token, TOKEN_OPEN_PAREN))
			{
				/* Reads to the next token after the TOKEN_CLOSE_PAREN */
				skipArgumentList(token);
			}
		}
	} while (! isType (token, TOKEN_CLOSE_PAREN));
}

static void parseType (tokenInfo *const token)
{
	tokenInfo *const name = newToken ();
	vString * saveScope = vStringNew ();

	vStringCopy(saveScope, token->scope);
	/* If a scope has been set, add it to the name */
	addToScope (name, token->scope);
	readToken (name);
	if (isType (name, TOKEN_IDENTIFIER))
	{
		readToken (token);
		if (isKeyword (token, KEYWORD_is))
		{
			readToken (token);
			addToScope (token, name->string);
			switch (token->keyword)
			{
				case KEYWORD_record:
				case KEYWORD_object:
					makeSqlTag (name, SQLTAG_RECORD);
					parseRecord (token);
					break;

				case KEYWORD_table:
					makeSqlTag (name, SQLTAG_TABLE);
					break;

				case KEYWORD_ref:
					readToken (token);
					if (isKeyword (token, KEYWORD_cursor))
						makeSqlTag (name, SQLTAG_CURSOR);
					break;

				default: break;
			}
			vStringClear (token->scope);
		}
	}
	vStringCopy(token->scope, saveScope);
	deleteToken (name);
	vStringDelete(saveScope);
}

static void parseSimple (tokenInfo *const token, const sqlKind kind)
{
	/* This will simply make the tagname from the first word found */
	readToken (token);
	if (isType (token, TOKEN_IDENTIFIER) ||
			isType (token, TOKEN_STRING))
		makeSqlTag (token, kind);
}

static void parseDeclare (tokenInfo *const token, const boolean local)
{
	/*
	 * PL/SQL declares are of this format:
	 *	  IS|AS
	 *	  [declare]
	 *		 CURSOR curname ...
	 *		 varname1 datatype;
	 *		 varname2 datatype;
	 *		 varname3 datatype;
	 *	  begin
	 */

	if (isKeyword (token, KEYWORD_declare))
		readToken (token);
	while (! isKeyword (token, KEYWORD_begin) && ! isKeyword (token, KEYWORD_end))
	{
		switch (token->keyword)
		{
			case KEYWORD_cursor:	parseSimple (token, SQLTAG_CURSOR); break;
			case KEYWORD_function:	parseSubProgram (token); break;
			case KEYWORD_procedure: parseSubProgram (token); break;
			case KEYWORD_subtype:	parseSimple (token, SQLTAG_SUBTYPE); break;
			case KEYWORD_trigger:	parseSimple (token, SQLTAG_TRIGGER); break;
			case KEYWORD_type:		parseType (token); break;

			default:
				if (isType (token, TOKEN_IDENTIFIER))
				{
					if (local)
					{
						makeSqlTag (token, SQLTAG_LOCAL_VARIABLE);
					} 
					else 
					{
						makeSqlTag (token, SQLTAG_VARIABLE);
					}
				}
				break;
		}
		findToken (token, TOKEN_SEMICOLON);
		readToken (token);
	}
}

static void parseDeclareANSI (tokenInfo *const token, const boolean local)
{
	tokenInfo *const type = newToken ();
	/*
	 * ANSI declares are of this format:
	 *	 BEGIN
	 *		 DECLARE varname1 datatype;
	 *		 DECLARE varname2 datatype;
	 *		 ...
	 *
	 * This differ from PL/SQL where DECLARE preceeds the BEGIN block
	 * and the DECLARE keyword is not repeated.
	 */
	while (isKeyword (token, KEYWORD_declare))
	{
		readToken (token);
		readToken (type);

		if (isKeyword (type, KEYWORD_cursor))
			makeSqlTag (token, SQLTAG_CURSOR);
		else if (isKeyword (token, KEYWORD_local) &&
				isKeyword (type, KEYWORD_temporary))
		{
			/*
			 * DECLARE LOCAL TEMPORARY TABLE table_name (
			 *	  c1 int,
			 *	  c2 int
			 * );
			 */
			readToken (token);
			if (isKeyword (token, KEYWORD_table))
			{
				readToken (token);
				if (isType(token, TOKEN_IDENTIFIER) || 
						isType(token, TOKEN_STRING) )
				{
					makeSqlTag (token, SQLTAG_TABLE);
				}
			}
		}
		else if (isType (token, TOKEN_IDENTIFIER) || 
				isType (token, TOKEN_STRING))
		{
			if (local)
				makeSqlTag (token, SQLTAG_LOCAL_VARIABLE);
			else
				makeSqlTag (token, SQLTAG_VARIABLE);
		}
		findToken (token, TOKEN_SEMICOLON);
		readToken (token);
	}
	deleteToken (type);
}

static void parseLabel (tokenInfo *const token)
{
	/*
	 * A label has this format:
	 *	   <<tobacco_dependency>>
	 *	   DECLARE
	 *		  v_senator VARCHAR2(100) := 'THURMOND, JESSE';
	 *	   BEGIN
	 *		  IF total_contributions (v_senator, 'TOBACCO') > 25000
	 *		  THEN
	 *			 <<alochol_dependency>>
	 *			 DECLARE
	 *				v_senator VARCHAR2(100) := 'WHATEVERIT, TAKES';
	 *			 BEGIN
	 *				...
	 */

	Assert (isType (token, TOKEN_BLOCK_LABEL_BEGIN));
	readToken (token);
	if (isType (token, TOKEN_IDENTIFIER))
	{
		makeSqlTag (token, SQLTAG_BLOCK_LABEL);
		readToken (token);		  /* read end of label */
	}
}

static void parseStatements (tokenInfo *const token, const boolean exit_on_endif )
{
	boolean isAnsi   = TRUE;
	boolean stmtTerm = FALSE;
	do
	{

		if (isType (token, TOKEN_BLOCK_LABEL_BEGIN))
			parseLabel (token);
		else
		{
			switch (token->keyword)
			{
				case KEYWORD_exception:
					/*
					 * EXCEPTION
					 *	 <exception handler>;
					 *
					 * Where an exception handler could be:
					 *	 BEGIN
					 *		WHEN OTHERS THEN
					 *			x := x + 3;
					 *	 END;
					 * In this case we need to skip this keyword and 
					 * move on to the next token without reading until
					 * TOKEN_SEMICOLON;
					 */
					readToken (token);
					continue;

				case KEYWORD_when:
					/*
					 * WHEN statements can be used in exception clauses
					 * and CASE statements.  The CASE statement should skip
					 * these given below we skip over to an END statement.
					 * But for an exception clause, we can have:
					 *	   EXCEPTION
					 *		   WHEN OTHERS THEN
					 *		   BEGIN
					 *				  x := x + 3;
					 *		   END;
					 * If we skip to the TOKEN_SEMICOLON, we miss the begin
					 * of a nested BEGIN END block.  So read the next token
					 * after the THEN and restart the LOOP.
					 */
					while (! isKeyword (token, KEYWORD_then))
						readToken (token);

					readToken (token);
					continue;

				case KEYWORD_if:
					/*
					 * We do not want to look for a ; since for an empty
					 * IF block, it would skip over the END.
					 *	IF...THEN
					 *	END IF;
					 *
					 *	IF...THEN
					 *	ELSE
					 *	END IF;
					 *
					 *	IF...THEN
					 *	ELSEIF...THEN
					 *	ELSE
					 *	END IF;
					 *
					 *	or non-ANSI
					 *	IF ...
					 *	BEGIN
					 *	END
					 */
					while ( ! isKeyword (token, KEYWORD_then)  &&
							! isKeyword (token, KEYWORD_begin) )
					{
						readToken (token);
					}

					if( isKeyword (token, KEYWORD_begin ) )
					{
						isAnsi = FALSE;
						parseBlock(token, FALSE);

						/*
						 * Handle the non-Ansi IF blocks.
						 * parseBlock consumes the END, so if the next
						 * token in a command terminator (like GO)
						 * we know we are done with this statement.
						 */
						if ( isCmdTerm (token) )
							stmtTerm = TRUE;
					}
					else
					{
						readToken (token);

						while( ! (isKeyword (token, KEYWORD_end ) ||
						          isKeyword (token, KEYWORD_endif ) ) 
								)
						{
							if ( isKeyword (token, KEYWORD_else) ||
									isKeyword (token, KEYWORD_elseif)    )
								readToken (token);

							parseStatements (token, TRUE);

							if ( isCmdTerm(token) )
								readToken (token);

						}

						/* 
						 * parseStatements returns when it finds an END, an IF
						 * should follow the END for ANSI anyway.
						 *	IF...THEN
						 *	END IF;
						 */
						if( isKeyword (token, KEYWORD_end ) )
							readToken (token);

						if( isKeyword (token, KEYWORD_if ) || isKeyword (token, KEYWORD_endif ) )
						{
							readToken (token);
							if ( isCmdTerm(token) )
								stmtTerm = TRUE;
						} 
						else 
						{
							/*
							 * Well we need to do something here.
							 * There are lots of different END statements
							 * END;
							 * END CASE;
							 * ENDIF;
							 * ENDCASE;
							 */
						}
					}
					break;

				case KEYWORD_loop:
				case KEYWORD_case:
				case KEYWORD_for:
					/*
					 *	LOOP...
					 *	END LOOP;
					 *	
					 *	CASE
					 *	WHEN '1' THEN
					 *	END CASE;
					 *	
					 *	FOR loop_name AS cursor_name CURSOR FOR ...
					 *	DO
					 *	END FOR;
					 */
					if( isKeyword (token, KEYWORD_for ) )
					{
						/* loop name */
						readToken (token);
						/* AS */
						readToken (token);

						while ( ! isKeyword (token, KEYWORD_is) )
						{
							/*
							 * If this is not an AS keyword this is 
							 * not a proper FOR statement and should 
							 * simply be ignored
							 */
							return;
						}

						while ( ! isKeyword (token, KEYWORD_do) )
							readToken (token);
					}


					readToken (token);
					while( ! isKeyword (token, KEYWORD_end ) )
					{
						/*
						if ( isKeyword (token, KEYWORD_else) ||
								isKeyword (token, KEYWORD_elseif)    )
							readToken (token);
							*/

						parseStatements (token, FALSE);

						if ( isCmdTerm(token) )
							readToken (token);
					}


					if( isKeyword (token, KEYWORD_end ) )
						readToken (token);

					/*
					 * Typically ended with 
					 *    END LOOP [loop name];
					 *    END CASE
					 *    END FOR [loop name];
					 */
					if ( isKeyword (token, KEYWORD_loop) ||
							isKeyword (token, KEYWORD_case) ||
							isKeyword (token, KEYWORD_for)    )
						readToken (token);

					if ( isCmdTerm(token) )
						stmtTerm = TRUE;

					break;

				case KEYWORD_create:
					readToken (token);
					parseKeywords(token);
					break;

				case KEYWORD_declare:
				case KEYWORD_begin:
					parseBlock (token, TRUE);
					break;

				case KEYWORD_end:
					break;

				default:
					readToken (token);
					break;
			}
			/*
			 * Not all statements must end in a semi-colon 
			 *	   begin
			 *		   if current publisher <> 'publish' then
			 *			 signal UE_FailStatement
			 *		   end if
			 *	   end;
			 * The last statement prior to an end ("signal" above) does
			 * not need a semi-colon, nor does the end if, since it is 
			 * also the last statement prior to the end of the block.
			 *
			 * So we must read to the first semi-colon or an END block
			 */
			while ( ! stmtTerm                               && 
					! (   isKeyword (token, KEYWORD_end)     ||
						 (isCmdTerm(token))              )	 
					)
			{
				if (   isKeyword (token, KEYWORD_endif)   &&
						 exit_on_endif                   )	  
					return;

				if (isType (token, TOKEN_COLON) )
				{
					/*
					 * A : can signal a loop name 
					 *    myloop:
					 *    LOOP 
					 *        LEAVE myloop;
					 *    END LOOP;
					 * Unfortunately, labels do not have a
					 * cmd terminator, therefore we have to check 
					 * if the next token is a keyword and process 
					 * it accordingly.
					 */
					readToken (token);
					if ( isKeyword (token, KEYWORD_loop) ||
							isKeyword (token, KEYWORD_while) ||
							isKeyword (token, KEYWORD_for) )
						/* parseStatements (token); */
						return;
				}

				readToken (token);

				if (isType (token, TOKEN_OPEN_PAREN)  ||
				    isType (token, TOKEN_OPEN_CURLY)  ||
				    isType (token, TOKEN_OPEN_SQUARE)  )
						skipToMatched (token);

				/*
				 * Since we know how to parse various statements 
				 * if we detect them, parse them to completion
				 */
				if (isType (token, TOKEN_BLOCK_LABEL_BEGIN) ||
						isKeyword (token, KEYWORD_exception) ||
						isKeyword (token, KEYWORD_loop) ||
						isKeyword (token, KEYWORD_case) ||
						isKeyword (token, KEYWORD_for) ||
						isKeyword (token, KEYWORD_begin)    )
					parseStatements (token, FALSE);
				else if (isKeyword (token, KEYWORD_if))
					parseStatements (token, TRUE);

			}
		}
		/*
		 * We assumed earlier all statements ended with a command terminator.
		 * See comment above, now, only read if the current token 
		 * is not a command terminator.
		 */
		if ( isCmdTerm(token) && ! stmtTerm )
			stmtTerm = TRUE;
				
	} while (! isKeyword (token, KEYWORD_end) && 
			 ! (exit_on_endif && isKeyword (token, KEYWORD_endif) ) && 
			 ! stmtTerm );
}

static void parseBlock (tokenInfo *const token, const boolean local)
{
	if (isType (token, TOKEN_BLOCK_LABEL_BEGIN))
	{
		parseLabel (token);
		readToken (token);
	}
	if (! isKeyword (token, KEYWORD_begin))
	{
		readToken (token);
		/*
		 * These are Oracle style declares which generally come
		 * between an IS/AS and BEGIN block.
		 */
		parseDeclare (token, local);
	}
	if (isKeyword (token, KEYWORD_begin))
	{
		readToken (token);
		/*
		 * Check for ANSI declarations which always follow
		 * a BEGIN statement.  This routine will not advance
		 * the token if none are found.
		 */
		parseDeclareANSI (token, local);
		token->begin_end_nest_lvl++;
		while (! isKeyword (token, KEYWORD_end))
		{
			parseStatements (token, FALSE);

			if ( isCmdTerm(token) )
				readToken (token);
		}
		token->begin_end_nest_lvl--;

		/*
		 * Read the next token (we will assume
		 * it is the command delimiter)
		 */
		readToken (token);
		
		/*
		 * Check if the END block is terminated
		 */
		if ( !isCmdTerm (token)	)
		{
			/*
			 * Not sure what to do here at the moment.
			 * I think the routine that calls parseBlock
			 * must expect the next token has already
			 * been read since it is possible this 
			 * token is not a command delimiter.
			 */
			/* findCmdTerm (token, FALSE); */
		}
	}
}

static void parsePackage (tokenInfo *const token)
{
	/* 
	 * Packages can be specified in a number of ways:
	 *		CREATE OR REPLACE PACKAGE pkg_name AS
	 * or
	 *		CREATE OR REPLACE PACKAGE owner.pkg_name AS
	 * or by specifying a package body
	 *	   CREATE OR REPLACE PACKAGE BODY pkg_name AS
	 *	   CREATE OR REPLACE PACKAGE BODY owner.pkg_name AS
 */
	tokenInfo *const name = newToken ();
	readToken (name);
	if (isKeyword (name, KEYWORD_body))
	{
		/*
		 * Ignore the BODY tag since we will process
		 * the body or prototypes in the same manner
		 */
		readToken (name);
	}
	/* Check for owner.pkg_name */
	while (! isKeyword (token, KEYWORD_is))
	{
		readToken (token);
		if ( isType(token, TOKEN_PERIOD) )
		{
			readToken (name);
		}
	}
	if (isKeyword (token, KEYWORD_is))
	{
		if (isType (name, TOKEN_IDENTIFIER) ||
				isType (name, TOKEN_STRING))
			makeSqlTag (name, SQLTAG_PACKAGE);
		addToScope (token, name->string);
		parseBlock (token, FALSE);
		vStringClear (token->scope);
	}
	findCmdTerm (token, FALSE);
	deleteToken (name);
}

static void parseTable (tokenInfo *const token)
{
	tokenInfo *const name = newToken ();

	/*
	 * This deals with these formats:
	 *	   create table t1 (c1 int);
	 *	   create global tempoary table t2 (c1 int);
	 *	   create table "t3" (c1 int);
	 *	   create table bob.t4 (c1 int);
	 *	   create table bob."t5" (c1 int);
	 *	   create table "bob"."t6" (c1 int);
	 *	   create table bob."t7" (c1 int);
	 * Proxy tables use this format:
	 *	   create existing table bob."t7" AT '...';
	 * SQL Server and Sybase formats
     *     create table OnlyTable (
     *     create table dbo.HasOwner (
     *     create table [dbo].[HasOwnerSquare] (
     *     create table master.dbo.HasDb (
     *     create table master..HasDbNoOwner (
     *     create table [master].dbo.[HasDbAndOwnerSquare] (
     *     create table [master]..[HasDbNoOwnerSquare] (
	 */

	/* This could be a database, owner or table name */
	readToken (name);
	if (isType (name, TOKEN_OPEN_SQUARE))
	{
		readToken (name);
		/* Read close square */
		readToken (token);
	} 
	readToken (token);
	if (isType (token, TOKEN_PERIOD))
	{
		/* 
		 * This could be a owner or table name.
		 * But this is also a special case since the table can be 
		 * referenced with a blank owner:
		 *     dbname..tablename
		 */
		readToken (name);
		if (isType (name, TOKEN_OPEN_SQUARE))
		{
			readToken (name);
			/* Read close square */
			readToken (token);
		} 
		/* Check if a blank name was provided */
		if (isType (name, TOKEN_PERIOD))
		{
			readToken (name);
			if (isType (name, TOKEN_OPEN_SQUARE))
			{
				readToken (name);
				/* Read close square */
				readToken (token);
			} 
		}
		readToken (token);
		if (isType (token, TOKEN_PERIOD))
		{
			/* This can only be the table name */
			readToken (name);
			if (isType (name, TOKEN_OPEN_SQUARE))
			{
				readToken (name);
				/* Read close square */
				readToken (token);
			} 
			readToken (token);
		}
	}
	if (isType (token, TOKEN_OPEN_PAREN))
	{
		if (isType (name, TOKEN_IDENTIFIER) ||
				isType (name, TOKEN_STRING))
		{
			makeSqlTag (name, SQLTAG_TABLE);
			vStringCopy(token->scope, name->string);
			parseRecord (token);
			vStringClear (token->scope);
		}
	} 
	else if (isKeyword (token, KEYWORD_at))
	{
		if (isType (name, TOKEN_IDENTIFIER))
		{
			makeSqlTag (name, SQLTAG_TABLE);
		}
	}
	findCmdTerm (token, FALSE);
	deleteToken (name);
}

static void parseIndex (tokenInfo *const token)
{
	tokenInfo *const name  = newToken ();
	tokenInfo *const owner = newToken ();

	/*
	 * This deals with these formats
	 *	   create index i1 on t1(c1) create index "i2" on t1(c1) 
	 *	   create virtual unique clustered index "i3" on t1(c1) 
	 *	   create unique clustered index "i4" on t1(c1) 
	 *	   create clustered index "i5" on t1(c1) 
	 *	   create bitmap index "i6" on t1(c1)
	 */

	readToken (name);
	readToken (token);
	if (isType (token, TOKEN_PERIOD))
	{
		readToken (name);
		readToken (token);
	}
	if ( isKeyword (token, KEYWORD_on) &&
			(isType (name, TOKEN_IDENTIFIER) || isType (name, TOKEN_STRING) ) )
	{
		readToken (owner);
		readToken (token);
		if (isType (token, TOKEN_PERIOD))
		{
			readToken (owner);
			readToken (token);
		}
		addToScope(name, owner->string);
		makeSqlTag (name, SQLTAG_INDEX);
	}
	findCmdTerm (token, FALSE);
	deleteToken (name);
	deleteToken (owner);
}

static void parseEvent (tokenInfo *const token)
{
	tokenInfo *const name = newToken ();

	/*
	 * This deals with these formats
	 *	   create event e1 handler begin end;
	 *	   create event "e2" handler begin end;
	 *	   create event dba."e3" handler begin end;
	 *	   create event "dba"."e4" handler begin end;
	 */

	readToken (name);
	readToken (token);
	if (isType (token, TOKEN_PERIOD))
	{
		readToken (name);
	}
	while (! (isKeyword (token, KEYWORD_handler) ||
				(isType (token, TOKEN_SEMICOLON)))	  )
	{
		readToken (token);
	}

	if ( isKeyword (token, KEYWORD_handler) ||
			isType (token, TOKEN_SEMICOLON)   )
	{
		makeSqlTag (name, SQLTAG_EVENT);
	}

	if (isKeyword (token, KEYWORD_handler))
	{
		readToken (token);
		if ( isKeyword (token, KEYWORD_begin) )
		{
			parseBlock (token, TRUE);
		}
		findCmdTerm (token, TRUE);
	}
	deleteToken (name);
}

static void parseTrigger (tokenInfo *const token)
{
	tokenInfo *const name  = newToken ();
	tokenInfo *const table = newToken ();

	/*
	 * This deals with these formats
	 *	   create or replace trigger tr1 begin end;
	 *	   create trigger "tr2" begin end;
	 *	   drop trigger "droptr1";
	 *	   create trigger "tr3" CALL sp_something();
	 *	   create trigger "owner"."tr4" begin end;
	 *	   create trigger "tr5" not valid;
	 *	   create trigger "tr6" begin end;
	 */

	readToken (name);
	readToken (token);
	if (isType (token, TOKEN_PERIOD))
	{
		readToken (name);
		readToken (token);
	}

	while ( !isKeyword (token, KEYWORD_on) &&
			!isCmdTerm (token)		)
	{
		readToken (token);
	}

	/*if (! isType (token, TOKEN_SEMICOLON) ) */
	if (! isCmdTerm (token) )
	{
		readToken (table);
		readToken (token);
		if (isType (token, TOKEN_PERIOD))
		{
			readToken (table);
			readToken (token);
		}

		while (! (isKeyword (token, KEYWORD_begin) ||
					(isKeyword (token, KEYWORD_call)) ||
					(	isCmdTerm (token)))    )
		{
			if ( isKeyword (token, KEYWORD_declare) )
			{
				addToScope(token, name->string);
				parseDeclare(token, TRUE);
				vStringClear(token->scope);
			}
			else
				readToken (token);
		}

		if ( isKeyword (token, KEYWORD_begin) || 
				isKeyword (token, KEYWORD_call)   )
		{
			addToScope(name, table->string);
			makeSqlTag (name, SQLTAG_TRIGGER);
			addToScope(token, table->string);
			if ( isKeyword (token, KEYWORD_begin) )
			{
				parseBlock (token, TRUE);
			}
			vStringClear(token->scope);
		}
	}

	findCmdTerm (token, TRUE);
	deleteToken (name);
	deleteToken (table);
}

static void parsePublication (tokenInfo *const token)
{
	tokenInfo *const name = newToken ();

	/*
	 * This deals with these formats
	 *	   create or replace publication pu1 ()
	 *	   create publication "pu2" ()
	 *	   create publication dba."pu3" ()
	 *	   create publication "dba"."pu4" ()
	 */

	readToken (name);
	readToken (token);
	if (isType (token, TOKEN_PERIOD))
	{
		readToken (name);
		readToken (token);
	}
	if (isType (token, TOKEN_OPEN_PAREN))
	{
		if (isType (name, TOKEN_IDENTIFIER) ||
				isType (name, TOKEN_STRING))
		{
			makeSqlTag (name, SQLTAG_PUBLICATION);
		}
	}
	findCmdTerm (token, FALSE);
	deleteToken (name);
}

static void parseService (tokenInfo *const token)
{
	tokenInfo *const name = newToken ();

	/*
	 * This deals with these formats
	 *	   CREATE SERVICE s1 TYPE 'HTML' 
	 *		   AUTHORIZATION OFF USER DBA AS 
	 *		   SELECT * 
	 *			 FROM SYS.SYSTABLE;
	 *	   CREATE SERVICE "s2" TYPE 'HTML'
	 *		   AUTHORIZATION OFF USER DBA AS 
	 *		   CALL sp_Something();
	 */

	readToken (name);
	readToken (token);
	if (isKeyword (token, KEYWORD_type))
	{
		if (isType (name, TOKEN_IDENTIFIER) ||
				isType (name, TOKEN_STRING))
		{
			makeSqlTag (name, SQLTAG_SERVICE);
		}
	}
	findCmdTerm (token, FALSE);
	deleteToken (name);
}

static void parseDomain (tokenInfo *const token)
{
	tokenInfo *const name = newToken ();

	/*
	 * This deals with these formats
	 *	   CREATE DOMAIN|DATATYPE [AS] your_name ...;
	 */

	readToken (name);
	if (isKeyword (name, KEYWORD_is))
	{
		readToken (name);
	}
	readToken (token);
	if (isType (name, TOKEN_IDENTIFIER) ||
			isType (name, TOKEN_STRING))
	{
		makeSqlTag (name, SQLTAG_DOMAIN);
	}
	findCmdTerm (token, FALSE);
	deleteToken (name);
}

static void parseDrop (tokenInfo *const token)
{
	/*
	 * This deals with these formats
	 *	   DROP TABLE|PROCEDURE|DOMAIN|DATATYPE name;
	 *
	 * Just simply skip over these statements.
	 * They are often confused with PROCEDURE prototypes
	 * since the syntax is similar, this effectively deals with
	 * the issue for all types.
	 */

	findCmdTerm (token, FALSE);
}

static void parseVariable (tokenInfo *const token)
{
	tokenInfo *const name = newToken ();

	/*
	 * This deals with these formats
	 *	   create variable varname1 integer;
	 *	   create variable @varname2 integer;
	 *	   create variable "varname3" integer;
	 *	   drop   variable @varname3;
	 */

	readToken (name);
	readToken (token);
	if ( (isType (name, TOKEN_IDENTIFIER) || isType (name, TOKEN_STRING))
			&& !isType (token, TOKEN_SEMICOLON) )
	{
		makeSqlTag (name, SQLTAG_VARIABLE);
	}
	findCmdTerm (token, TRUE);

	deleteToken (name);
}

static void parseSynonym (tokenInfo *const token)
{
	tokenInfo *const name = newToken ();

	/*
	 * This deals with these formats
	 *	   create variable varname1 integer;
	 *	   create variable @varname2 integer;
	 *	   create variable "varname3" integer;
	 *	   drop   variable @varname3;
	 */

	readToken (name);
	readToken (token);
	if ( (isType (name, TOKEN_IDENTIFIER) || isType (name, TOKEN_STRING))
			&& isKeyword (token, KEYWORD_for) )
	{
		makeSqlTag (name, SQLTAG_SYNONYM);
	}
	findCmdTerm (token, TRUE);

	deleteToken (name);
}

static void parseView (tokenInfo *const token)
{
	tokenInfo *const name = newToken ();

	/*
	 * This deals with these formats
	 *	   create variable varname1 integer;
	 *	   create variable @varname2 integer;
	 *	   create variable "varname3" integer;
	 *	   drop   variable @varname3;
	 */

	readToken (name);
	readToken (token);
	if (isType (token, TOKEN_PERIOD))
	{
		readToken (name);
		readToken (token);
	}
	if ( isType (token, TOKEN_OPEN_PAREN) )
	{
		skipArgumentList(token);

	}

	while (!(isKeyword (token, KEYWORD_is) ||
				isType (token, TOKEN_SEMICOLON)
			))
	{
		readToken (token);
	}

	if ( (isType (name, TOKEN_IDENTIFIER) || isType (name, TOKEN_STRING))
			&& isKeyword (token, KEYWORD_is) )
	{
		makeSqlTag (name, SQLTAG_VIEW);
	}

	findCmdTerm (token, TRUE);

	deleteToken (name);
}

static void parseMLTable (tokenInfo *const token)
{
	tokenInfo *const version = newToken ();
	tokenInfo *const table	 = newToken ();
	tokenInfo *const event	 = newToken ();

	/*
	 * This deals with these formats
	 *	  call dbo.ml_add_table_script( 'version', 'table_name', 'event',
	 *		   'some SQL statement'
	 *		   );
	 */

	readToken (token);
	if ( isType (token, TOKEN_OPEN_PAREN) )
	{
		readToken (version);
		readToken (token);
		while (!(isType (token, TOKEN_COMMA) ||
					isType (token, TOKEN_CLOSE_PAREN)
				))
		{
			readToken (token);
		}

		if (isType (token, TOKEN_COMMA))
		{
			readToken (table);
			readToken (token);
			while (!(isType (token, TOKEN_COMMA) ||
						isType (token, TOKEN_CLOSE_PAREN)
					))
			{
				readToken (token);
			}

			if (isType (token, TOKEN_COMMA))
			{
				readToken (event);

				if (isType (version, TOKEN_STRING) && 
						isType (table, TOKEN_STRING) && 
						isType (event, TOKEN_STRING) )
				{
					addToScope(version, table->string);
					addToScope(version, event->string);
					makeSqlTag (version, SQLTAG_MLTABLE);
				}
			} 
			if( !isType (token, TOKEN_CLOSE_PAREN) )
				findToken (token, TOKEN_CLOSE_PAREN);
		} 
	}

	findCmdTerm (token, TRUE);

	deleteToken (version);
	deleteToken (table);
	deleteToken (event);
}

static void parseMLConn (tokenInfo *const token)
{
	tokenInfo *const version = newToken ();
	tokenInfo *const event	 = newToken ();

	/*
	 * This deals with these formats
	 *	  call ml_add_connection_script( 'version', 'event',
	 *		   'some SQL statement'
	 *		   );
	 */

	readToken (token);
	if ( isType (token, TOKEN_OPEN_PAREN) )
	{
		readToken (version);
		readToken (token);
		while (!(isType (token, TOKEN_COMMA) ||
					isType (token, TOKEN_CLOSE_PAREN)
				))
		{
			readToken (token);
		}

		if (isType (token, TOKEN_COMMA))
		{
			readToken (event);

			if (isType (version, TOKEN_STRING) && 
					isType (event, TOKEN_STRING) )
			{
				addToScope(version, event->string);
				makeSqlTag (version, SQLTAG_MLCONN);
			}
		} 
		if( !isType (token, TOKEN_CLOSE_PAREN) )
			findToken (token, TOKEN_CLOSE_PAREN);

	}

	findCmdTerm (token, TRUE);

	deleteToken (version);
	deleteToken (event);
}

static void parseMLProp (tokenInfo *const token)
{
	tokenInfo *const component     = newToken ();
	tokenInfo *const prop_set_name = newToken ();
	tokenInfo *const prop_name     = newToken ();

	/*
	 * This deals with these formats
     *   ml_add_property ( 
     *       'comp_name', 
     *       'prop_set_name', 
     *       'prop_name', 
     *       'prop_value'
     *   )
	 */

	readToken (token);
	if ( isType (token, TOKEN_OPEN_PAREN) )
	{
		readToken (component);
		readToken (token);
		while (!(isType (token, TOKEN_COMMA) ||
					isType (token, TOKEN_CLOSE_PAREN)
				))
		{
			readToken (token);
		}

		if (isType (token, TOKEN_COMMA))
		{
			readToken (prop_set_name);
			readToken (token);
			while (!(isType (token, TOKEN_COMMA) ||
						isType (token, TOKEN_CLOSE_PAREN)
					))
			{
				readToken (token);
			}

			if (isType (token, TOKEN_COMMA))
			{
				readToken (prop_name);

				if (isType (component, TOKEN_STRING) && 
						isType (prop_set_name, TOKEN_STRING) && 
						isType (prop_name, TOKEN_STRING) )
				{
					addToScope(component, prop_set_name->string);
					addToScope(component, prop_name->string);
					makeSqlTag (component, SQLTAG_MLPROP);
				}
			} 
			if( !isType (token, TOKEN_CLOSE_PAREN) )
				findToken (token, TOKEN_CLOSE_PAREN);
		} 
	}

	findCmdTerm (token, TRUE);

	deleteToken (component);
	deleteToken (prop_set_name);
	deleteToken (prop_name);
}

static void parseComment (tokenInfo *const token)
{
	/*
	 * This deals with this statement:
	 *	   COMMENT TO PRESERVE FORMAT ON PROCEDURE "DBA"."test" IS 
	 *	   {create PROCEDURE DBA."test"()
	 *	   BEGIN
	 *		signal dave;
	 *	   END
	 *	   }
	 *	   ;
	 * The comment can contain anything between the CURLY
	 * braces
	 *	   COMMENT ON USER "admin" IS
	 *			'Administration Group'
	 *			;
	 * Or it could be a simple string with no curly braces
	 */
	while (! isKeyword (token, KEYWORD_is))
	{
		readToken (token);
	}
	readToken (token);
	if ( isType(token, TOKEN_OPEN_CURLY) )
	{
		findToken (token, TOKEN_CLOSE_CURLY);
	}

	findCmdTerm (token, TRUE);
}


static void parseKeywords (tokenInfo *const token)
{
		switch (token->keyword)
		{
			case KEYWORD_begin:			parseBlock (token, FALSE); break;
			case KEYWORD_comment:		parseComment (token); break;
			case KEYWORD_cursor:		parseSimple (token, SQLTAG_CURSOR); break;
			case KEYWORD_datatype:		parseDomain (token); break;
			case KEYWORD_declare:		parseBlock (token, FALSE); break;
			case KEYWORD_domain:		parseDomain (token); break;
			case KEYWORD_drop:			parseDrop (token); break;
			case KEYWORD_event:			parseEvent (token); break;
			case KEYWORD_function:		parseSubProgram (token); break;
			case KEYWORD_if:			parseStatements (token, FALSE); break;
			case KEYWORD_index:			parseIndex (token); break;
			case KEYWORD_ml_table:		parseMLTable (token); break;
			case KEYWORD_ml_table_lang: parseMLTable (token); break;
			case KEYWORD_ml_table_dnet: parseMLTable (token); break;
			case KEYWORD_ml_table_java: parseMLTable (token); break;
			case KEYWORD_ml_table_chk:  parseMLTable (token); break;
			case KEYWORD_ml_conn:		parseMLConn (token); break;
			case KEYWORD_ml_conn_lang:	parseMLConn (token); break;
			case KEYWORD_ml_conn_dnet:	parseMLConn (token); break;
			case KEYWORD_ml_conn_java:	parseMLConn (token); break;
			case KEYWORD_ml_conn_chk:	parseMLConn (token); break;
			case KEYWORD_ml_prop:		parseMLProp (token); break;
			case KEYWORD_package:		parsePackage (token); break;
			case KEYWORD_procedure:		parseSubProgram (token); break;
			case KEYWORD_publication:	parsePublication (token); break;
			case KEYWORD_service:		parseService (token); break;
			case KEYWORD_subtype:		parseSimple (token, SQLTAG_SUBTYPE); break;
			case KEYWORD_synonym:		parseSynonym (token); break;
			case KEYWORD_table:			parseTable (token); break;
			case KEYWORD_trigger:		parseTrigger (token); break;
			case KEYWORD_type:			parseType (token); break;
			case KEYWORD_variable:		parseVariable (token); break;
			case KEYWORD_view:			parseView (token); break;
			default:				    break;
		}
}

static void parseSqlFile (tokenInfo *const token)
{
	do
	{
		readToken (token);

		if (isType (token, TOKEN_BLOCK_LABEL_BEGIN))
			parseLabel (token);
		else 
			parseKeywords (token);
	} while (! isKeyword (token, KEYWORD_end));
}

static void initialize (const langType language)
{
	Assert (sizeof (SqlKinds) / sizeof (SqlKinds [0]) == SQLTAG_COUNT);
	Lang_sql = language;
	buildSqlKeywordHash ();
}

static void findSqlTags (void)
{
	tokenInfo *const token = newToken ();
	exception_t exception = (exception_t) (setjmp (Exception));

	while (exception == ExceptionNone)
		parseSqlFile (token);

	deleteToken (token);
}

extern parserDefinition* SqlParser (void)
{
	static const char *const extensions [] = { "sql", NULL };
	parserDefinition* def = parserNew ("SQL");
	def->kinds		= SqlKinds;
	def->kindCount	= KIND_COUNT (SqlKinds);
	def->extensions = extensions;
	def->parser		= findSqlTags;
	def->initialize = initialize;
	return def;
}

/* vi:set tabstop=4 shiftwidth=4 noexpandtab: */
