Eigene DSL mit CongoCC erstellen (2)

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.

Schreibe einen Kommentar