“Every program attempts to expand until it can read mail. Those programs which cannot so expand are replaced by ones which can.“
Zawinski’s Law
Im ersten Beitrag zum Thema wurde eine einfache DSL zur Verarbeitung von CSV Dateien vorgestellt. In diesem Beitrag soll die DSL um einige weitere Feature ergänzt werden.
Auch wenn das Eingangszitat es nahe legt, wird CQL nicht um das Lesen von E-Mails erweitert. Eine DSL sollte nicht beliebig um Features ergänzt werden, die Einsatzdomäne und ihre Herausforderungen sollten immer Quelle für Verbesserungen sein. Dabei hilft der Einsatz eines Parser-Generators wie CongoCC, die Möglichkeiten einer DSL mit wenig Aufwand erheblich zu verbessern.
-- Example SELECT $1, $2 WHERE $1 like '(cat|dog)'; # Example SELECT ($1 + $2) AS summe WHERE summe > 15;
Die erste Ergänzung für CQL sind einzeilige Kommentar. Diese sind im SQL und AWK Stil möglich. Die zweite Ergänzung ist ein like Operator um Textvariablen nicht nur mit Konstanten und andere Variablen zu vergleichen, sondern auch reguläre Ausdrücke verwenden zu können. Als dritte Ergänzung können Aliase in Ausdrücken genutzt werden. Die vierte und letzte Ergänzung erlaubt es die (bislang nutzlose) FROM Klausel auszulassen.
Um die Kommentare mit CongoCC zu realisieren ist nur die folgende Definition in der CQL Grammatik zu ergänzen.
UNPARSED #Comment : <SINGLE_LINE_COMMENT : ("--"|"#") (~["\n"])* > ;
Für die anderen drei Ergänzungen muss die CQL Grammatik um die Token ALIAS
und AS
und die folgenden Änderungen an den Regeln ergänzt werden.
EqualityExpression : RelationalExpression [ (<EQUALS>|<DOUBLE_EQUALS>|<NOT_EQUALS>|<LIKE>) RelationalExpression ] ; ExpressionWithAlias : Expression [ <AS> <IDENTIFIER> ] ; Select #SelectStatement : <SELECT> ExpressionWithAlias ( <COMMA> ExpressionWithAlias )* [ <FROM> <IDENTIFIER> ] [ <WHERE> Expression ] [ <LIMIT> <INTEGER> ] [ <USING> StringLiteral ] <SEMICOLON> ;
Das LIKE Token wird bei den anderen Gleichheitsoperatoren aufgenommen. Alles weitere zum like Operator findet sich in der Implementierung.
Damit im SelectStatement
für eine Spalte ein Alias vergeben werden kann, wird die Expression
durch den Knoten ExpressionWithAlias
ersetzt. Eine ExpressionWithAlias
ist eine Expression
die optional von einem Token AS
und einem IDENTIFIER
ergänzt werden.
In der ersten Implementierung wurde der Großteil der Auswertung in einer Schleife durchgeführt. In der neue Implementierung wurden dies auf mehrere Schritte in einem Stream
verteilt.
private void handleReader(AtomicInteger nr, String filename, BufferedReader reader, PrintStream out) { AtomicInteger fnr = new AtomicInteger(); reader.lines().filter(l -> !l.startsWith("#")) .map(line -> createRecordContext(nr, filename, line, fnr)) .filter(context -> whereHandler.handle(context).get()) .map(this::generateOutput) .limit(limit) .forEach(out::println); }
Im Stream
werden im ersten Schritt alle Zeilen ausgefiltert, die mit einem #
beginnen. Damit werden Kommentare in den CSV Dateien ignoriert. Als nächstes werden die Zähler für nr
und fnr
inkrementiert und der RecordContext
erzeugt.
Danach werden alle RecordContext
Werte aus dem Stream gefiltert, deren Where-Ausdruck false
ergibt und danach die Ausgabezeile erzeugt. Damit auch nur die gewünschte Anzahl von Zeilen ausgegeben wird, wird diese mit limit
begrenzt und zuletzt werden die Zeilen in den angegebenen PrintStream
geschrieben.
private RecordContext createRecordContext(AtomicInteger nr, String filename, String line, AtomicInteger fnr) { nr.incrementAndGet(); fnr.incrementAndGet(); Map<String, Value<?>> values = new HashMap<>(); values.put("$0", new StringValue(line)); values.put("NR", new LongValue(nr.longValue())); values.put("FNR", new LongValue(fnr.longValue())); values.put("FILENAME", new StringValue(filename)); RecordContext recordContext = new RecordContext(line, line.split(readDelimiter), values); for (Entry<String, Integer> entry : aliases.entrySet()) { Value<?> value = columnHandlers.get(entry.getValue()).handle(recordContext); recordContext.values().put(entry.getKey(), value); } return recordContext; }
Bislang gab es eigene Attribute im RecordContext
für line
, filename
, nr
und fnr
. Da für jedes Alias dessen Wert zu Beginn ausgewertet und in eine Map
gespeichert wird, erfolgt der Zugriff auf die anderen vier Werte auch über die die Map
.
Die Auswertung der Aliase ermöglicht es, deren Werte nicht nur im WHERE Ausdruck zu verwenden, sondern auch in anderen Ausgabespalten. Lediglich die Verwendung in der Spalte, die das Alias definiert oder in Spalten links von der Definition mit einem Alias ist nicht möglich.
Damit sind die angesprochenen Ergänzungen an CQL auch schon implementiert und die nächsten Ideen warten darauf in diese oder eine andere DSL aufgenommen zu werden.