I've been trying to implement the Visitor Pattern to parse some specific SQL Statements into an internal object structure consisting of TableDefinition and ColumnDefinition objects.
This is a small (stripped down) portion from the grammar:
column_definition
: column_name datatype? column_constraint*
;
column_constraint
: ( K_CONSTRAINT name )?
( K_PRIMARY K_KEY ( K_CLUSTERED | K_NONCLUSTERED )? ( K_ASC | K_DESC )? K_AUTOINCREMENT?
| K_AUTOINCREMENT
| K_NOT? K_NULL
)
;
datatype
: K_CHAR ( '(' unsigned_integer ')' )? #char
| K_DATE #date
;
And here is one of the derived BaseVisitors which is meant to return ColumnDefinitions:
namespace SqlParser.Visitor
{
public class DataTypeVisitor: SqlAnywhereParserBaseVisitor<ColumnDefinition>
{
public override ColumnDefinition VisitColumn_definition([NotNull] SqlAnywhereParser.Column_definitionContext context)
{
var res = VisitChildren(context);
var constraint = (SqlAnywhereParser.Column_constraintContext[])context.column_constraint();
if (res != null) // Add NULL attributes
{
if (constraint.Any(c => c.K_NULL() != null && c.K_NOT() == null))
res.IsNullable = true;
if (constraint.Any(c => c.K_NULL() != null && c.K_NOT() != null))
res.IsNullable = false;
}
return res;
}
public override ColumnDefinition VisitChar([NotNull] SqlAnywhereParser.CharContext context)
{
return new ColumnDefinition()
{
DataType = DbType.StringFixedLength,
Length = int.Parse(context.unsigned_integer()?.GetText() ?? "1")
};
}
}
}
When I debug the process, I can observe how the call to VisitChildren goes into VisitChar which returns a ColumnDefinition object. When VisitChar completes and the cursor jumps back to continue in VisitColumn_definition the variable res is null.
Did I miss something crucial or did I misunderstand the visitor pattern? Before I tried VisitChildren I used to use the base.VisitColumn_definition(context) call, which basically only calls VisitChildren.
Does anyone have a hint, which mistakes I made? Why doesn't my ColumnDefinition result created at the VisitChar leaf bubble up?
Below is my testinput:
CREATE TABLE "DBA"."pbcattbl" (
"pbt_tnam" char(129) NOT NULL
,"pbt_tid" char(5) NULL
);
I found a solution:
protected override List<ColumnDefinition> AggregateResult(List<ColumnDefinition> aggregate, List<ColumnDefinition> nextResult)
{
if (aggregate != null && nextResult != null) aggregate.AddRange(nextResult);
return aggregate ?? nextResult;
}
I converted the result to List<ColumnDefinition> and added an appropriate override to AggregateResult.
Thank you @kaby76 for pointing me into the right direction with your comment. Also thanks to all others for the feedback and quick responses!