pdf (1820 Kb) - Fachgebiet Datenbanken und Informationssysteme ...
pdf (1820 Kb) - Fachgebiet Datenbanken und Informationssysteme ...
pdf (1820 Kb) - Fachgebiet Datenbanken und Informationssysteme ...
Erfolgreiche ePaper selbst erstellen
Machen Sie aus Ihren PDF Publikationen ein blätterbares Flipbook mit unserer einzigartigen Google optimierten e-Paper Software.
Leibniz Universität Hannover<br />
Fakultät für Elektrotechnik <strong>und</strong> Informatik<br />
Institut für Praktische Informatik<br />
<strong>Fachgebiet</strong> <strong>Datenbanken</strong> <strong>und</strong> <strong>Informationssysteme</strong><br />
Bachelorarbeit<br />
im Studiengang Informatik<br />
Relationenalgebra als Datenbankanfragesprache<br />
Stefanie Bernhardt<br />
Matr.-Nr. 2517460<br />
12. Mai 2009<br />
Prüfer <strong>und</strong> Betreuer: Prof. Dr. Udo Lipeck<br />
Zweitprüfer: Dr. Hans Hermann Brüggemann
Inhaltsverzeichnis<br />
1 Einleitung 3<br />
1.1 Zielsetzung der Arbeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3<br />
1.2 Gliederung der Arbeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3<br />
2 Gr<strong>und</strong>lagen 4<br />
2.1 Die Relationenalgebra . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4<br />
2.1.1 Gr<strong>und</strong>operationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4<br />
2.1.2 Ableitbare Operationen . . . . . . . . . . . . . . . . . . . . . . . . . 5<br />
2.1.3 Erweiterung der Relationenalgebra . . . . . . . . . . . . . . . . . . 6<br />
2.2 Prinzipieller Aufbau eines Compilers . . . . . . . . . . . . . . . . . . . . . 7<br />
2.3 JavaCC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8<br />
2.4 Die JavaCC-Grammatikdatei . . . . . . . . . . . . . . . . . . . . . . . . . . 9<br />
3 Anforderungen im Detail 12<br />
3.1 Mindestanforderungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12<br />
3.2 Verbesserungsmöglichkeiten . . . . . . . . . . . . . . . . . . . . . . . . . . 13<br />
4 Entwurf 14<br />
4.1 Syntax der Anfragesprache . . . . . . . . . . . . . . . . . . . . . . . . . . . 14<br />
4.2 Analyse der Eingabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16<br />
4.2.1 Lexikale Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16<br />
4.2.2 Syntaktische Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . 19<br />
4.2.3 Semantische Analyse: . . . . . . . . . . . . . . . . . . . . . . . . . . 34<br />
4.2.4 Die Symboltabelle . . . . . . . . . . . . . . . . . . . . . . . . . . . 38<br />
4.3 SQL-Code-Erzeugung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39<br />
5 Implementierung 48<br />
5.1 Package-Struktur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48<br />
5.2 Die Grammatikdatei <strong>und</strong> generierte Klassen . . . . . . . . . . . . . . . . . 48<br />
5.3 Beschreibung der nicht generierten Klassen . . . . . . . . . . . . . . . . . . 49<br />
6 Die Benutzerschnittstelle 56<br />
Abbildungsverzeichnis 58<br />
Tabellenverzeichnis 59<br />
2
Kapitel 1<br />
Einleitung<br />
1.1 Zielsetzung der Arbeit<br />
Ziel dieser Arbeit ist die Entwicklung einer Datenbankanfragesprache auf Basis der Relationenalgebra.<br />
Dazu soll eine graphische Benutzerschnittstelle bereitgestellt werden, die<br />
auf komfortable Weise die Eingabe von Anfragen in der Relationenalgebra ermöglicht,<br />
sowie Ausgaben <strong>und</strong> ggf. Fehlermeldungen anzeigt. Die Anfragen werden intern in ihrer<br />
Struktur analysiert <strong>und</strong> auf Fehler geprüft. Im Fall eines Fehlers soll eine aussagekräftige<br />
Fehlermeldung ausgegeben werden. Ist die Anfrage fehlerfrei, so wird sie vereinfacht, in<br />
SQL übersetzt <strong>und</strong> an das Oracle-DBS gesendet. Die Ergebnistabellen sollen dem Nutzer<br />
in einem Ausgabebereich angezeigt werden.<br />
Der Hauptteil dieser Arbeit wird sich mit der Analyse, der Vereinfachung <strong>und</strong> der Übersetzung<br />
von Anfragen beschäftigen. Daneben spielen die Bereitstellung der Benutzerschnittstelle,<br />
die Verbindung zum Datenbanksystem <strong>und</strong> die Erweiterbarkeit eine große<br />
Rolle.<br />
Nach seiner Fertigstellung soll das Programm Studenten als Lernmittel dienen, um den<br />
sonst nur theoretischen Umgang mit der Relationenalgebra durch praktische Anwendung<br />
besser verinnerlichen zu können.<br />
1.2 Gliederung der Arbeit<br />
Im Anschluss an diese kleine Einleitung sollen zunächst die nötigen Gr<strong>und</strong>lagen erläutert<br />
werden, bevor etwas detaillierter auf die Anforderungen eingegangen wird. Im vierten Kapitel<br />
soll der Entwurf vorgestellt werden. Hier wird zunächst die Anfragesprache entworfen.<br />
Die folgenden Unter-Kapitel beschäftigen sich mit dem Entwurf der einzelnen Analyse<strong>und</strong><br />
Übersetzungs-Komponenten. Kapitel 5 gibt einen Überblick über die implementierten<br />
Klassen. Abschließend wird die Benutzeroberfläche vorgestellt.<br />
3
Kapitel 2<br />
Gr<strong>und</strong>lagen<br />
2.1 Die Relationenalgebra<br />
Die Relationenalgebra ist die Menge aller endlichen Relationen zusammen mit Operationen.<br />
Durch Kombination von Operationen lassen sich Terme formulieren, die man als<br />
prozedurale Datenbank-Anfragesprache auffassen kann.<br />
Im Folgenden werden die einzelnen relationalen Operationen beschrieben. Dabei sei jeweils<br />
für die Relation R das Schema (A 1 : D 1 , ..., A n : D n ), sowie für die Relation S das<br />
Schema (B 1 : D 1 , ..., B m : D m ) mit Attributen A i , B j <strong>und</strong> Datentypen D k vorausgesetzt.<br />
2.1.1 Gr<strong>und</strong>operationen<br />
• R ∪ S<br />
Die Vereinigung R ∪ S vereinigt alle Tupel zweier Relationen R <strong>und</strong> S unter der<br />
Vorraussetzung, dass beide Relationen die gleichen Schemata besitzen.<br />
Voraussetzung: Schema(R)=Schema(S)=(A 1 , ..., A n )<br />
Ergebnis-Schema: (A 1 , ..., A n )<br />
Ergeibnis-Relation: {t | t ∈ R ∨ t ∈ S}<br />
• R − S<br />
Die Differenz R − S liefert alle R-Tupel, die nicht in S enthalten sind.<br />
Voraussetzung: Schema(R)=Schema(S)=(A 1 , ..., A n )<br />
Ergebnis-Schema: (A 1 , ..., A n )<br />
Ergebnis-Relation: {t | t ∈ R ∧ t /∈ S}<br />
• σ ϕ (R)<br />
Die Selektion σ ϕ (R) liefert eine Teilmenge aller Tupel einer Relation R entsprechend<br />
der Selektionsformel ϕ. Dabei ist ϕ entweder eine atomare Selektionsformel<br />
oder eine Zusammensetzung atomarer Selektionsformeln mit den logischen Operatoren<br />
∧, ∨, ¬, ⇒, ⇔. Eine atomare Selektionsformel sei ϕ = θ (α 1 , ..., α n ) mit θ:<br />
n-stelliger Vergleichsoperator <strong>und</strong> α k : Attributterm aus R-Attributen, Datentyp-<br />
Operationen <strong>und</strong> Datentyp-Konstanten.<br />
Ergebnis-Schema: Schema(R)=(A 1 , ..., A n )<br />
Ergebnis-Relation: {t | t ∈ R ∧ t erfüllt ϕ}<br />
4
• π Ā (R)<br />
Die Projektion π Ā (R) liefert alle Spalten einer Relation R entsprechend der Attributliste<br />
Ā. Die Attributliste besteht aus R-Attributtermen <strong>und</strong> jeweils einem<br />
optionalen Aliasnamen.<br />
Voraussetzung: Ā = (α 1 C 1 , ..., α k C k )) α i : Attributterm, C i : Aliasname<br />
Ergebnis-Schema: (C 1 , .., C k )<br />
Ergebnis-Relation: {t : (C 1 , .., C k ) | r ∈ R, t = (π α1 (r) , ..., π αk (r))}<br />
• R × S<br />
Das Kartesische Produkt R×S bildet alle möglichen Kombinationen von Tupeln<br />
aus R <strong>und</strong> S. Vorraussetzung ist, dass die Attributmengen von R <strong>und</strong> S disjunkt<br />
sind.<br />
Voraussetzung: {A 1 , ..., A n } ∩ {B 1 , ..., B m } = ∅<br />
Ergebnis-Schema: (A 1 , ..., A n , B 1 , ..., B m )<br />
Ergebnis-Relation: {t | r ∈ R, s ∈ S, t = r · s}<br />
2.1.2 Ableitbare Operationen<br />
• R ∩ S = R − (R − S)<br />
Der Durchschnitt R ∩ S liefert alle Tupel, die in R <strong>und</strong> S enthalten sind.<br />
Voraussetzung: Schema(R)=Schema(S)=(A 1 , ..., A n )<br />
Ergebnis-Schema: (A 1 , ..., A n )<br />
Ergebnis-Relation: {t | t ∈ R ∧ t ∈ S}<br />
• R ⊲⊳ ϕ<br />
S = σ ϕ (R × S)<br />
Der Verb<strong>und</strong> bzw. Join liefert alle Kombinationen von R- <strong>und</strong> S-Tupeln bei denen<br />
die Verb<strong>und</strong>sbedingung ϕ erfüllt ist. Dabei ist die Verb<strong>und</strong>sbedingung ϕ eine<br />
Mengen von ∧-verknüpften Vergleichen A i θB j von Attributtermen von R <strong>und</strong> S mit<br />
Vergleichsoperator θ. Für θ ≡= handelt es sich um einen Equijoin.<br />
Ergebnis-Schema: (A 1 , ..., A n ) · (B 1 , ..., B m )<br />
Ergebnis-Relation: {r · s | r ∈ R ∧ s ∈ S ∧ r, s erfüllen ϕ}<br />
• R ⊲⊳ S = R ⊲⊳ ϕ S mit ϕ = ∧ A i = B j S, wobei A i , B j jeweils namensgleiche<br />
Attribute von R <strong>und</strong> S sind. Der Natural Join ist somit ein Equijoin über alle<br />
namensgleichen Attribute von R <strong>und</strong> S.<br />
Ergebnis-Schema: {A 1 , ..., A n } ∪ {B 1 , ..., B m }<br />
Ergebnis-Relation: {t mit Attributen {A 1 , ..., A n } ∪ {B 1 , ..., B m } |<br />
π A1 ,...,A n<br />
(t) ∈ R ∧ π B1 ,...,B m<br />
(t) ∈ S<br />
• R ⊲< S<br />
Der Semijoin R ⊲< ϕ S = π Attribute(R) (R ⊲⊳ ϕ S) liefert alle R-Tupel, die mindestens<br />
einen Joinpartner in S haben.<br />
Ergebnis-Schema: Schema(R)=(A 1 , ..., A n )<br />
Ergebnis-Relation: {t ∈ R | ∃ (s ∈ S) t, s erfüllen ϕ}<br />
5
• R ⊲< ϕ<br />
S<br />
Der Anti-Semijoin R ⊲< ϕ S = R − (R ⊲< ϕ S) liefert alle R-Tupel, die keine<br />
Joinpartner in S haben.<br />
Ergebnis-Schema: Schema(R)=(A 1 , ..., A n )<br />
Ergebnis-Relation: {t ∈ R | ¬∃ (s ∈ S) t, s erfüllen ϕ}<br />
• R ⊐⊲⊳⊏ ϕ S<br />
Der Outerjoin liefert alle Tupel aus dem Join zwischen R <strong>und</strong> S mit zusätzlich<br />
allen R <strong>und</strong> S-Tupeln, die keinen Joinpartner haben (mit Nullwerten aufgefüllt)<br />
Ergebnis-Schema: (A 1 , ..., A n ) · (B 1 , ..., B m )<br />
Ergebnis-Relation: {t · s | t ∈ R , s ∈ S , t, s erfüllen ϕ}<br />
∪ { t · (⊥, · · · , ⊥) | t ∈ R , t /∈ (R ⊲⊳ S) t erfüllt ϕ}<br />
∪ { (⊥, · · · , ⊥) · s | s /∈ (R ⊲⊳ S) s erfüllt ϕ}<br />
Varianten:Left Outer Join, Right Outer Join<br />
Hierbei werden nur die Tupel der linken (bzw rechten) Relation mit Nullwerten<br />
aufgefüllt.<br />
• Division R ÷ S<br />
Für R(A 1 , ..., A n ) <strong>und</strong> S(B 1 , ..., B m ) <strong>und</strong> unter der Voraussetzung (B 1 , ..., B m ) =<br />
(A n−m+1 , ..., A n ) selektiert die Division R ÷ S alle (A 1 , ..., A n−m )-Tupel aus R die<br />
mit allen S-Tupeln in R auftreten.<br />
Voraussetzung: Schema(S) ⊆ Schema(R), S ≠ ∅<br />
Ergebnis-Schema: (A 1 , . . . , A n−m ),<br />
Ergebnis-Relation: { t: (A 1 , . . . , A n−m ) | ∃(r ∈R) ( t=π A1 ,...,A n−m<br />
(r) ∧ ∀(s∈S) t·s ∈<br />
R ) }<br />
• Umbenennungen<br />
Jeder Relationsoperand darf mit einem Alias versehen werden. Für eine Relation<br />
R <strong>und</strong> ein Alias X, ist die Schreibweise (R X). Auf die Attribute der umbenannten<br />
R-Relation wird mit X.A i zugegriffen.<br />
2.1.3 Erweiterung der Relationenalgebra<br />
• Γ Ḡ# ¯F (R)<br />
Die Gruppierung Γ Ḡ# ¯F (R) liefert alle Spalten entsprechend ¯F von Tupel-Gruppen<br />
mit gleichen Ḡ-Werten. ¯F enthält eine Liste von gruppeninvarianten R-Attributtermen<br />
<strong>und</strong> auf R-Attributterme angewendete Aggregatfunktionen wie count, min,<br />
max, sum, avg. Für Ḡ = ∅ bilden alle Tupel eine Gruppe.<br />
Beispiel: Γ ∅#count(∗) (ST UDENT EN)<br />
Voraussetzung: ¯F = (α1 C 1 , ..., α k C k )) α i : Attributterm oder Aggr.-Funktion, C i :<br />
Aliasname<br />
Ergebnis-Schema: (C 1 , .., C k )<br />
6
2.2 Prinzipieller Aufbau eines Compilers<br />
Im Allgemeinen bestehen Compiler aus sechs Funktionseinheiten mit klar abgegrenzten<br />
Aufgabenbereichen. Dieses Vorgehen ermöglicht eine hohe Wiederverwendbarkeit einzelner<br />
Komponenten <strong>und</strong> vereinfacht nachträgliche Änderungen durch Austauschen einzelner<br />
Einheiten. In der Praxis gibt es durchaus Abweichungen in der Anzahl der Komponenten,<br />
sowie in der Verteilung ihrer Aufgaben, was aber an der gr<strong>und</strong>legenden Funktionsweise<br />
nichts ändert. Im folgenden Abschnitt werden kurz die typischen Funktionseinheiten<br />
erläutert.<br />
Lexikale Analyse<br />
Die aktive Komponente der lexikalen Analyse ist der Scanner bzw. Lexer. Der Scanner liest<br />
den zu übersetzenden Code zeichenweise ein <strong>und</strong> erzeugt daraus eine Tokenfolge. Jedes<br />
Token besteht dabei aus einer Tokenklasse <strong>und</strong> ggf. einem Tokenwert. Ein Tokenwert ist<br />
ein Zeiger auf den entsprechenden Eintrag in der Symboltabelle, in welcher Informationen<br />
über die auftretenden Tokens abgespeichert werden.<br />
Syntaktische Analyse<br />
Die aktive Komponente der syntaktischen Analyse ist der Parser. Er analysiert die vom<br />
Scanner erzeugte Tokenfolge anhand einer Grammatik, die die Syntax der Quellsprache<br />
beschreibt, <strong>und</strong> erzeugt daraus einen Syntaxbaum.<br />
Semantische Analyse<br />
Die letzte Analysephase ist die semantische Analyse. Die Hauptaufgaben der semantischen<br />
Analyse sind die Datentypprüfung <strong>und</strong> die Untersuchung von Gültigkeitsbereichen. Bei<br />
behebbaren Typfehlern kann ggf. eine Typanpassung vorgenommen werden.<br />
Zwischencodeerzeugung<br />
Damit ist die Analyse der Eingabe abgeschlossen. In manchen Fällen wird an dieser Stelle<br />
schon direkt der Zielcode erzeugt. Im Allgemeinen wird jedoch zunächst ein Zwischencode<br />
erzeugt, der sich leicht produzieren lässt <strong>und</strong> sich leicht in die Zielsprache übersetzen lässt.<br />
Der Zwischencode kann als Basis für verschiedene Übersetzungen in ähnliche Sprachen<br />
dienen <strong>und</strong> soll Optimierungen vereinfachen.<br />
Codeoptimierung<br />
Der Zwischencode wird nun in Hinblick auf Laufzeitverhalten <strong>und</strong> Speicherplatzbedarf<br />
optimiert. Dazu werden zum Beispiel überflüssige Berechnungen entfernt oder Anweisungen<br />
verschoben, sofern sie im Zielcode dadurch seltener ausgeführt werden müssen <strong>und</strong><br />
die Verschiebung semantisch nichts verändert.<br />
Zielcodeerzeugung<br />
In der letzten Phase wird nun aus dem optimierten Zwischencode der Zielcode erzeugt.<br />
7
2.3 JavaCC<br />
JavaCC steht für Java Compiler Compiler <strong>und</strong> ist gleichzeitig Scanner- <strong>und</strong> Parsergenerator,<br />
der in Java implementiert ist <strong>und</strong> Java-Code erzeugt. JavaCC ist Open Source <strong>und</strong><br />
unter den Bedingungen der BSD-Lizenz herausgegeben. Optional kann statt Scanner <strong>und</strong><br />
Parser auch nur eine der Komponenten erzeugt werden. Ein einfacher generierter Scanner<br />
(TokenManager) erzeugt eine Tokenfolge <strong>und</strong> speichert für jedes Token den Tokenwert, die<br />
Tokenklasse <strong>und</strong> die Position. Der Tokenmanager wirft Fehler vom Typ TokenMgrError,<br />
die die Fehlerquelle gut beschreiben. Für Fehler in der Eingabe wird eine Fehlermeldung<br />
mit der genauen Position des unerwarteten Zeichens <strong>und</strong> einigen zusätzlichen Informationen<br />
ausgegeben. Auch interne Fehler, wie Endlosschleifen oder der Versuch einer zweiten<br />
Instanzierung des Tokenmanagers werden identifiziert. Der generierte Parser ist ein LL(n)-<br />
Parser, wobei standardmäßig n=1 ist. Das Lookahead kann aber nicht nur optional größer<br />
eingestellt werden, sondern auch dynamisch während des Parsens angepasst werden. (ParseException<br />
noch erklären )<br />
Einfache Scanner <strong>und</strong> Parser können mit wenig Aufwand erzeugt werden. JavaCC bietet<br />
aber auch zahlreiche Möglichkeiten komplexere Strukturen zu scannen <strong>und</strong> zu parsen.<br />
Für den Tokenmanager können verschiedene Zustände (lexical states) definiert werden,<br />
in denen er unterschiedliche Token erkennt, was zum Beispiel zur Handhabung von Kommentaren<br />
hilfreich sein kann. Neben normalen Token <strong>und</strong> zu überlesenden Zeichenketten<br />
lassen sich SpecialTokens definieren, die an jeder beliebigen Stelle im Programm unabhängig<br />
von der Grammatik auftreten dürfen <strong>und</strong> keinen Einfluss auf den Parsingvorgang<br />
haben. Um dies zu gewährleisten tauchen die SpecialToken in der normalen Tokenfolge<br />
nicht auf. Stattdessen besitzt jedes Token einen zweite Referenz, auf ein SpecialToken<br />
unmittelbar vor dem Token. Jedes SpecialToken besitzt eine Referenz zum nächsten SpecialToken.<br />
Somit kann bei Bedarf auf die normale Tokenfolge ohne Specialtokens, auf die<br />
Specialtokenfolge <strong>und</strong> auf die Position der SpecialTokens innerhalb der Tokenfolge zugegriffen<br />
werden. Zusätzlich gibt es noch eine vierte Möglichkeit mit gescannten Zeichen<br />
umzugehen, dabei wird die eingelesene Zeichenkette zunächst zwischengespeichert <strong>und</strong><br />
dem nächsten erkannten Token zugefügt. Auch diese Funktion kann zum Beispiel beim<br />
Umgang mit Kommentaren hilfreich sein.<br />
Die Token für die Lexikale Analyse, sowie die Grammatik für den Parser, Einstellungen<br />
des Compilers <strong>und</strong> Javacode-Fragmente werden zusammen in einer Grammatikdatei<br />
definiert. Dabei werden die Token entweder als Strings oder als reguläre Ausdrücke notiert.<br />
Die Parser-Produktionen werden in EBNF oder in Javacode angegeben. Aus der<br />
Grammatikdatei erzeugt der JavaC-Compiler alle nötigen Dateien. Die erzeugten Klassen<br />
umfassen dabei die Klassen für den Parser, den Scanner (TokenManager) <strong>und</strong> eine<br />
Constants-Klasse, welche die definierten Token auf Konstanten abbildet. Diese Klassen<br />
werden bei jedem Kompiliervorgang neu erzeugt. Zusätzlich werden, sofern sie nicht bereits<br />
vorhanden sind, vier weitere Klassen erzeugt: Token.java, SimpleCharStream.java,<br />
TokenMgrError.java <strong>und</strong> ParseException.java.<br />
Ein großer Vorteil von JavaCC ist, dass man sehr viele Möglichkeiten hat die Erzeugung<br />
von Klassen <strong>und</strong> deren Verhalten zu beeinflussen. Beispielsweise bietet JavaCC verschiedene<br />
Optionen, die zu Beginn in der Grammatikdatei auf die gewünschten Werte gesetzt<br />
werden können. Zum Beispiel lässt sich mit den Optionen USER_CHAR_STREAM <strong>und</strong><br />
USER_TOKEN_MANAGER die Erzeugung der konkreten Charstream- <strong>und</strong> TokenManager-<br />
Klassen unterbinden <strong>und</strong> stattdessen Interfaces erzeugt. Über die Option BUILD_PARSER<br />
8
lässt sich die Erzeugung des Parsers abstellen. COMMON_TOKEN_ACTION ist eine<br />
Option die standardmäßig false ist. Setzt man sie auf true, so wird nach jedem gescannten<br />
Token die Methode CommonTokenActen(Token t) aufgerufen, die man selbst definieren<br />
kann. Dies eignet sich zum Beispiel sehr gut um die Symboltabelle mit den verschiedenen<br />
Token zu füllen. Natürlich gibt es noch eine Vielzahl weiterer Optionen, auf die ich aber<br />
an dieser Stelle aber nicht weiter eingehen möchte.<br />
Zusätzlich lässt sich die Grammatikdatei an vielen Stellen mit Javacode anreichern. Das<br />
Gr<strong>und</strong>gerüst der Parserklasse wird standardmäßig in der Grammatikdatei definiert. Für<br />
den Scanner lassen sich neben der CommonTokenAction auch lexikale Aktionen definieren:<br />
Jedem regulärem Ausdruck kann ein Java-Block folgen, in dem die Aktion definiert<br />
wird, die nach Erkennung eines Wortes des Ausdrucks ausgeführt werden soll. Ähnlich<br />
können für den Parser parser_actions definiert werden, die bei Anwendung einer Produktion<br />
ausgeführt werden, dies könnten zum Beispiel Anweisungen zum Konstruieren eines<br />
Syntaxbaumes sein. Zum Erstellen eines Syntaxbaumes kann auch optional ein Präprozessor<br />
wie JJTree oder JTB vorgeschaltet werden. In diesem Fall wird die eigentliche<br />
.jj-Grammatikdatei aus einer, im Fall von JJTree, .jjt-Grammatikdatei generiert. JJTree<br />
ergänzt den Code dabei um den Aufbau des Syntaxbaumes.<br />
Man erkennt schnell dass sich JavaCC sowohl für einfache, sowie auch für komplexe<br />
Scanner <strong>und</strong> Parser eignet. Die von vorne herein vorhandene gute Fehlererkennung, die<br />
zahlreichen Einstellmöglichkeiten <strong>und</strong> vor Allem die Möglichkeit einzelne Produktion mit<br />
Javacode anzureichern, machen die Nutzung dieses Tools für diese Arbeit möglich. Wie<br />
bereits oben erwähnt, bringt die Nutzung eines Generators erhebliche Vorteile in Punkt<br />
Erweiterbarkeit mit sich, weswegen JavaCC hier auf jeden Fall benutzt werden soll.<br />
2.4 Die JavaCC-Grammatikdatei<br />
Der Aufbau der Grammatikdatei soll hier nur grob erläutert werden. Eine ausführliche<br />
Beschreibung findet sich unter https://javacc.dev.java.net/doc/javaccgrm.html .<br />
Die Grammatikdatei beginnt bei Bedarf mit einem Optionenblock. Einige der möglichen<br />
Optionen habe ich oben bereits erwähnt, JavaCC bietet aber eine Vielzahl weiterer möglicher<br />
Optionen zur Anpassung der generierten Klassen an die jeweiligen Bedürfnisse des<br />
Entwicklers. Der Optionenblock wird mit dem Schlüsselwort options eingeleitet. Zwischen<br />
geschweiften Klammern werden dann den Optionen die gewünschten Werte zugewiesen.<br />
Ein Optionenblock könnte also wie folgt aussehen:<br />
options<br />
{<br />
LOOKAHEAD=2;<br />
IGNORECASE=true;<br />
COMMON_TOKEN_ACTION=true;<br />
}<br />
Möchte man die Standardeinstellungen beibehalten, kann der Optionenblock weggelassen<br />
werden.<br />
Als nächstes folgt compiliation_unit, eingeschlossen in die Schlüsselwörter<br />
PARSER_BEGIN(parser_name) <strong>und</strong> PARSER_END(parser_name). Hier erfolgt die<br />
Definition der Parser-Klasse, die beim Kompiliervorgang von JavaCC mit Code gefüllt<br />
werden soll. Außerdem kann hier eine Package-Deklaration angegeben sein, die für alle ge-<br />
9
nerierten Klassen gelten soll, so wie Import-Anweisungen für die Parser <strong>und</strong> TokenManger-<br />
Klassen. Ein Beispiel für eine compilation_unit:<br />
PARSER_BEGIN(TestParser)<br />
package foo;<br />
public class TestParser {<br />
public static void main(String args[]) throws ParseException {<br />
eg1 parser = new eg1(System.in);<br />
try {<br />
TestParser.StartSymbol();<br />
System.out.println("Thank␣you.");<br />
} catch (Exception e) {<br />
System.out.println(e.getMessage());<br />
TestParser.ReInit(System.in);<br />
}<br />
}<br />
}<br />
PARSER_END(TestParser)<br />
Anschließend müssen die Produktionen für den Scanner <strong>und</strong> den Parser definiert werden.<br />
Der gesamte Aufbau der Grammatikdatei ist auf der javacc-Homepage in EBNF dokumentiert.<br />
Da die Produktionen sehr unterschiedlich aufgebaut sein können, ist die Definition<br />
in EBNF hier auch sehr sinnvoll, weswegen ich diese übernehme.<br />
Eine regular_expression_production wird durch eins der vier Schlüsselwörter TOKEN,<br />
SKIP, SPECIAL_TOKEN oder MORE eingeleitet, die den Umgang mit Wörtern dieses<br />
Ausdrucks festlegen. Gegebenenfalls wird dem Schlüsselwort eine Liste der Zustände, in<br />
denen der reguläre Ausdruck erkannt werden soll, vorangestellt.<br />
regular_expression_production ::=<br />
["" | ""]<br />
( TOKEN | SKIP | SPECIAL_TOKEN | MORE ) [IGNORE_CASE]":"<br />
"{" regexpr_spec ( | regexpr_spec )∗ "}"<br />
regexpr_spec ::=<br />
regular_expression [java_block_for_lexical_action][":" identifier_for_lexical_state ]<br />
regular_expression ::=<br />
java_string_literal<br />
| ""<br />
| ""<br />
| ""<br />
complex_regular_expression_choices ::=<br />
complex_regular_expression ( "|" complex_regular_expression )∗<br />
complex_regular_expression ::=<br />
( complex_regular_expression_unit )∗<br />
complex_regular_expression_unit ::=<br />
java_string_literal<br />
| ""<br />
| character_list<br />
| "(" complex_regular_expression_choices ")" [ "+" | "∗" | "" ]<br />
Beispiel:<br />
10
TOKEN :<br />
{<br />
<br />
| <br />
| <br />
| <br />
| <br />
}<br />
Das für die regular_expression angegebene Symbol "#"dient der Markierung von privaten<br />
regulären Ausdrücken, die nur zur Hilfe definiert werden <strong>und</strong> selbst kein Token darstellen.<br />
Die Methode CommonTokenAction sowie Variablen <strong>und</strong> Methoden, die für lexikale Aktionen<br />
benötigt werden an einer zentralen Stelle definiert, eingeleitet durch das Schlüsselwort<br />
TOKEN_MGR_DECLS.<br />
token_manager_decls ::= "TOKEN_MGR_DECLS" ":" java_block<br />
Die Produktionen der Grammatik für den Parser werden in BNF notiert. Da jedes nichtterminale<br />
Symbol nach der Generierung des Parsers durch eine Methode dargestellt wird,<br />
ist es durchaus möglich für Nichtterminale Übergabeparameter <strong>und</strong> Rückgabewerte zu definieren,<br />
weswegen die Nichtterminalen in der Grammatikdatei mit einem Java-Methodenkopf<br />
inklusive Rückgabewert <strong>und</strong> Parameterliste definiert werden. Nach einem ":"folgt ein<br />
Java-Block, der entweder leer ist oder Deklarationen <strong>und</strong> Anweisungen enthält, die bei<br />
jeder Anwendung der Produktion ausgeführt werden sollen. Die Deklarationen gelten für<br />
alle Produktionen. Im Methodenrumpf ist die rechte Seite der BNF-Produktion definiert.<br />
Nichtterminale, die hier auftreten, werden wie Methodenaufrufe notiert, Terminale sind<br />
entweder einzelne Zeichen oder Tokens. An jeder beliebigen Position der Produktion kann<br />
ein weiterer Java-Block definiert werden, der ausgeführt wird, sobald er passiert wird.<br />
bnf_production ::=<br />
java_access_modifier java_return_type java_identifier "(" java_parameter_list ")" ":"<br />
java_block<br />
"{" expansion_choices "}"<br />
expansion_choices ::= expansion ( "|" expansion )∗<br />
expansion ::= ( expansion_unit )∗<br />
expansion_unit ::= local_lookahead<br />
| java_block<br />
| "(" expansion_choices ")" [ "+" | "∗" | "" ]<br />
| "[" expansion_choices "]"<br />
| [ java_assignment_lhs "=" ] regular_expression<br />
| [ java_assignment_lhs "=" ] java_identifier "(" java_expression_list ")"<br />
Es gibt noch eine zweite Möglichkeit Produktionen für den Parser anzugeben. Die Produktion<br />
wird dabei komplett in Javacode notiert <strong>und</strong> durch das Schlüsselwort JAVACODE<br />
eingeleitet. Da diese Variante aber für Grammatiken gedacht ist, die mit der kontextfreien<br />
Darstellung der BNF nicht auskommen, wird sie für diese Arbeit nicht benötigt.<br />
11
Kapitel 3<br />
Anforderungen im Detail<br />
3.1 Mindestanforderungen<br />
Die Benutzerschnittstelle<br />
Die graphische Benutzerschnittstelle soll dem Nutzer eine bequeme Eingabe der Anfragen<br />
ermöglichen <strong>und</strong> muss in der Lage sein, Fehlermeldungen <strong>und</strong> die Ergebnistabellen anzuzeigen.<br />
Dazu soll sie jeweils ein eigenes Teilfenster bereitstellen, wobei die Fenster für<br />
die Ergebnistabellen <strong>und</strong> die Eingabe natürlich entsprechend groß sein müssen. Ist der<br />
dargestellte Inhalt zu groß für den Anzeigebereich, so soll der Überlauf über Scrollbalken<br />
erreichbar sein. Außerdem soll die Copy&Paste-Funktion unterstützt werden.<br />
Die Sprache<br />
Die zu entwerfende Anfragesprache soll sich an der Relationenalgebra orientieren, wie<br />
sie in der Vorlesung „Gr<strong>und</strong>lagen der DBS“ im Sommersemester 08 eingeführt wurde.<br />
Dabei können griechische Symbole durch Schlüsselwörter ersetzt werden <strong>und</strong> die Eingabe<br />
linear, also ohne Tiefstellung von Parametern, erfolgen. Die Sprache sollte natürlich<br />
unter der Voraussetzung, dass man die Relationenalgebra kennt, möglichst intuitiv sein.<br />
Zu implementieren sind zunächst alle Gr<strong>und</strong>operationen, der Gamma-Operator, sowie<br />
alle abgeleiteten Operationen mit Ausnahme des Outerjoins <strong>und</strong> der Division. Die relationenalgebraische<br />
Definition der Attributterme, lässt einen sehr komplexen Aufbau zu,<br />
weswegen diese zunächst nur eingeschränkt implementiert werden sollen. Es soll mit Zeichenketten<br />
<strong>und</strong> Zahlen umgegangen werden können. Die Syntax der Anfragesprache ist<br />
über eine kontextfreie Grammatik zu definieren.<br />
Das Ausführungssystem<br />
Eine eingegebene Anfrage soll korrekt analysiert <strong>und</strong> in SQL übersetzt werden. Bei Fehlern<br />
in der Anfrage sind aussagekräftige Fehlermeldungen auszugeben, die dem Nutzer<br />
eine schnelle Korrektur ermöglichen. Die Übersetzung sollte mindestens nach dem Prinzip<br />
geschachtelter From-Klauseln erfolgen, wobei für jeden Operanden einer Operation<br />
eine Unteranfrage in der From-Klausel des jeweiligen SQL-Blocks ausgeführt wird. Da<br />
dieses Vorgehen zu einer stark verschachtelten SQL-Anfrage führt, die nicht nur schwer<br />
lesbar, sondern auch für das DBMS schwer optimierbar ist, ist ein besserer Übersetzungsalgorithmus<br />
natürlich wünschenswerter. Es soll zunächst ausreichen, wenn der erzeugte<br />
12
SQL-Code nur auf kleine <strong>Datenbanken</strong> angewendet werden kann. Neben der Analyse <strong>und</strong><br />
der Übersetzung ist noch die Zusammenarbeit mit dem Datenbanksystem zu regeln. Es<br />
muss möglich sein, eine Verbindung per Login aufzubauen, die Anfrage auszuführen <strong>und</strong><br />
das Ergebnis entgegen zu nehmen. Außerdem muss es möglich sein, Fehlermeldungen von<br />
Oracle entgegen zu nehmen <strong>und</strong> dem Nutzer als Laufzeitfehler anzuzeigen.<br />
3.2 Verbesserungsmöglichkeiten<br />
Der Umgang mit großen Datenmengen muss ermöglicht werden. Außerdem klammert die<br />
in den Mindestanforderungen festgelegte Funktionalität klammert noch einige Möglichkeiten<br />
der Relationenalgebra aus. Die Operationen Division <strong>und</strong> Outerjoin (damit auch<br />
der Left- <strong>und</strong> Right-Outerjoin) müssen noch integriert werden. Der Umgang mit dem<br />
Datentyp Date fehlt <strong>und</strong> die Attributterme sind bisher nur eingeschränkt implementiert.<br />
Außerdem muss der Umgang mit großen Datenmengen ermöglicht werden. Diese Erweiterungen<br />
sollten natürlich die höchste Priorität erhalten.<br />
In SQL gibt es durch die Order-By-Klausel die Möglichkeit Ergebnistabellen nach beliebigen<br />
Spalten sortieren zu lassen, was sich auch auf die zu entwickelnde Relationenalgebra-<br />
Anfragesprache übertragen lässt. Hierbei wäre es denkbar am Ende einer Anfrage die<br />
textuelle Eingabe einer Order-by-Anweisung zuzulassen oder stattdessen eine Order-by-<br />
Option auf der Benutzeroberfläche zur Verfügung zu stellen, bei der die jeweiligen Spalten<br />
eingetragen oder eventuell sogar ausgewählt werden könnten.<br />
Durch einen frühzeitigen Verbindungsaufbau zum DBS, wäre es auch möglich den Nutzer<br />
allgemein bei der Eingabe von Relationen- <strong>und</strong> Spaltennamen zu unterstützen.<br />
Zur besseren Bedienbarkeit <strong>und</strong> Lesbarkeit kann eine Syntaxunterstützung eingebaut<br />
werden. Es wäre in einigen Situationen auch Vorteilhaft, wenn die Möglichkeit besteht,<br />
den erzeugten SQL-Code auf der Benutzerschnittstelle auszugeben.<br />
Desweiteren bietet es sich an, dem Nutzer die Möglichkeit der Übungsabgabe <strong>und</strong> eine<br />
Benutzungshistorie zur Verfügung zu stellen. Auch die Bereitstellung einer englischen<br />
Version der Benutzeroberfläche wäre denkbar.<br />
Die Relationenalgebra sieht allgemein nur lesende Zugriffe auf <strong>Datenbanken</strong> vor, daher<br />
würde es zu weit gehen über eine Integrierung von Create/Update-Kommandos nachzudenken.<br />
Um Datenbank-Relationen zu erzeugen oder zu ändern ware also eine SQL-<br />
Schnittstelle nötig. Die Benutzerfre<strong>und</strong>lichkeit könnte hier allerdings dadurch erhöht werden,<br />
dass ein SQL-Modus angeboten wird, in dem der Nutzer statt Relationenalgebra-Anfragen,<br />
SQL-Anweisungen eingeben kann. Dies würde die Nutzung einer zweiten Schnittstelle<br />
für schreibende Zugriffe auf die Datenbank unnötig machen.<br />
13
Kapitel 4<br />
Entwurf<br />
4.1 Syntax der Anfragesprache<br />
Die Syntax der Anfragesprache soll möglichst intuitiv sein, das heißt sich möglichst gut<br />
an die Syntax der Relationenalgebra anlehnen. Da die Eingabe von den für die Relationenalgebra<br />
typischen Symbolen <strong>und</strong> Indizes am Computer allerdings schwierig ist, soll die<br />
Syntax hier etwas abgeändert werden.<br />
Die relationenalgebraischen Symbole werden durch Bezeichner ersetzt, welche natürlich<br />
der Bequemlichkeit wegen nicht zu lang sein sollten <strong>und</strong> trotzdem eindeutig <strong>und</strong> intuitiv<br />
auf das zugeordnete Symbol schließen lassen müssen. Die definierten Bezeichner sind der<br />
Tabelle 4.1 zu entnehmen. Der einheitlichen Darstellung wegen sind in der Tabelle alle<br />
Schlüsselworte in Großbuchstaben dargestellt, die zu entwickelnde Sprache soll aber bezüglich<br />
der Schlüsselworte nicht case-sensitiv sein. Die Eingaben „SEL“, „sel“ oder „Sel“<br />
werden gleich behandelt.<br />
Die Bedingungen <strong>und</strong> Attributlisten, die gewöhnlich im Index eines Symbols auftauchen,<br />
werden den Symbolen nun in eckigen Klammern nachgestellt. An der syntaktischen Struktur<br />
der Anfragen soll gegenüber der Relationenalgebra nichts verändert werden, jedoch<br />
gibt es vorerst ein paar Einschränkungen bzgl. der Sprachmächtigkeit:<br />
1. Es wird auf den Datentyp Date <strong>und</strong> auf die Operation Division verzichtet.<br />
2. Es wird auf die logischen Operatoren „=>“ <strong>und</strong> „ verzichtet.<br />
3. Ein mehr als zweistelliger Vergleich in einer Bedingung ist nur durch eine Und-<br />
Verknüpfung von zweistelligen Vergleichsoperationen möglich. Ausdruck (a) ist damit<br />
unzulässig, Ausdruck (b) zulässig.<br />
(a) SEL[A
Operation Symbol Sprachelement Beispiel<br />
Selektion σ SEL SEL[A>10](R)<br />
Projektion π PROJ PROJ[A Aneu, C](R)<br />
Vereinigung ∪ UNION R UNION S<br />
Durchschnitt ∩ INTERSECT R INTERSECT S<br />
Differenz - MINUS R MINUS S<br />
Kartesisches Produkt × CARTESIAN R CARTESIAN S<br />
Join ⊲⊳ JOIN R JOIN[A=B] S<br />
Natural Join ⊲⊳ NJOIN R NJOIN S<br />
Semijoin ⊲< SJOIN R SJOIN[A=B] S<br />
Anti-Semijoin ⊲< ASJOIN R ASJOIN[A=B] S<br />
Gruppierung Γ GROUP GROUP[A # A, avg(B)](R)<br />
Logisches Und ∧ AND SEL[A>2 AND B>2](R)<br />
Logisches Oder ∨ OR SEL[A>2 OR B>2](R)<br />
Logisches Nicht ! NOT SEL[NOT A=B] S<br />
Leere Menge ∅ {} GROUP[{} # count(*)](R)<br />
Tabelle 4.1: Sprachelemente<br />
Was an dieser Stelle noch anzumerken ist, ist die Klammerung. Die unären Operationen<br />
Selektion, Projektion <strong>und</strong> Gruppierung klammern ihre Operanden generell. Die ist auch<br />
aus der Relationenalgebra bekannt. Intuitiv klammert man einen Operanden einer binären<br />
Operation, wenn es sich bei dem Operanden wieder um eine binäre Operation handelt,<br />
um die Ausführungsreihenfolge eindeutig festzulegen. Für die zu entwickelnde Sprache soll<br />
eine Klammerung von Operanden gr<strong>und</strong>sätzlich immer möglich sein. Tritt eine zweistellige<br />
Operation als Operand auf, so wird eine Klammerung vorausgesetzt. Ein Ausdruck der<br />
Form ’1.’ wäre damit unzulässig <strong>und</strong> müsste geklammert werden. Die Ausdrücke 2. <strong>und</strong><br />
3. wären zulässig. Auch eine Klammerung der gesamten Anfrage ist möglich.<br />
1. R ASJOIN[A=B] S UNION T<br />
2. (R ASJOIN[A=B] S) UNION T<br />
3. R ASJOIN[A=B] (S UNION T)<br />
Die zu implementierenden Operatoren <strong>und</strong> Operationen können der folgenden Tabelle<br />
entnommen werden. Syntax- <strong>und</strong> Prioritätsregeln entsprechen dabei den bekannten<br />
Regeln aus SQL.<br />
Gruppe<br />
Operatoren/Operationen<br />
logische Operatoren AND, OR, NOT<br />
Vergleichsoperatoren =, , , >=, =
4.2 Analyse der Eingabe<br />
4.2.1 Lexikale Analyse<br />
Für den Scanner sind in erster Linie die regulären Ausdrücke für die Token zu definieren.<br />
Das zeichenweise Lesen der Eingabe, die Zuordnung zu den Token <strong>und</strong> die Erstellung<br />
der Tokenfolge, erledigt der generierte Scanner ohne weiteres Zutun. Für jedes Token<br />
t wird dabei automatisch die Tokenklasse, die zugehörige Zeichenkette in der Eingabe,<br />
sowie deren Position in den Variablen t.kind, t.image, t.beginLine <strong>und</strong> t.beginColumn<br />
abgespeichert.<br />
Definition der Tokenklassen<br />
Es müssen zunächst alle Schlüsselwörter der Sprache in regulären Ausdrücken untergebracht<br />
werden. Da meistens einer Operation bzw. einem Operator genau ein Schlüsselwort<br />
zugeordnet ist, wird für den Namen der Tokenklasse einfach das zugehörige Schlüsselwort<br />
gewählt, der jeweilige reguläre Ausdruck entspricht dem Schlüsselwort als Zeichenkette.<br />
Der reguläre Ausdruck für die Tokenklasse CONCAT wäre zum Beispiel einfach „concat“.<br />
Auf Groß- <strong>und</strong> Kleinschreibung muss hierbei nicht geachtet werden, da diese durch die<br />
JavaCC-Option „IGNORE_CASE“ ignoriert wird. Auf diese Weise werden definiert:<br />
1. Je eine Tokenklasse für jeden boolschen Operator, also and, or <strong>und</strong> not.<br />
2. Je eine Tokenklasse für die Vergleichsoperatoren between, like <strong>und</strong> in, sowie je eine<br />
Tokenklasse für die Schlüsselwörter is <strong>und</strong> null.<br />
3. Je eine Tokenklasse für die Schlüsselwörter aller String-Operationen, also concat,<br />
substr, length, instr, lpad, rpad, replace, trim, upper, lower, <strong>und</strong> initcap.<br />
4. Ebenso für die Schlüsselwörter der numerischen <strong>und</strong> Datentyp-unabhängigen Operationen,<br />
also für ro<strong>und</strong>, trunc, mod, nvl, nvl2, nullif <strong>und</strong> coalesce. .<br />
Sowie je eine Tokenklasse für die Aggregatfunktionen count, sum, min, max, avg.<br />
Da für die relationenalgebraischen Operationen zum Teil mehrere Schlüsselwörter zugelassen<br />
sind, sind die zugehörigen regulären Ausdrücke nicht mehr ganz trivial. Sie sind<br />
der Tabelle 4.3 zu entnehmen.<br />
Die bisher nicht aufgeführten Vergleichsoperatoren verhalten sich syntaktisch völlig gleich.<br />
Da als Operand eines Vergleichsoperators auch kein Vergleich zugelassen ist, unterscheiden<br />
sie sich auch nicht bezüglich ihrer Priorität <strong>und</strong> können daher in einer Tokenklasse<br />
zusammengefasst werden.<br />
COMP: „=“ | „“ | „>=“ | „
Tokenklasse Regulärer Ausdruck<br />
PROJ „proj“ | „pj“<br />
SEL<br />
„sel“<br />
GROUP „goup“ | „gp“<br />
JOIN „join“<br />
NJOIN „natural join“ | „njoin“ | „nj“<br />
SJOIN „semijoin“ | „sjoin“ | „sj“<br />
ASJOIN „antisemijoin“ | „asjoin“ | „asj“<br />
OJOIN „outer join“ | „ojoin“ | „oj“<br />
ROJOIN „right outer join“ | „rojoin“ | „roj“<br />
LOJOIN „left outer join“ | „lojoin“ | „loj“<br />
CARTESIAN „cartesian“ | „cross join“<br />
UNION „union “<br />
INTERSECT „intersect“<br />
MINUS „minus“<br />
Tabelle 4.3: Tokenklassen-Definitionen für Relationenalgebraische Operationen<br />
Tokenklasse Regulärer Ausdruck<br />
APLUS „+“<br />
AMINUS „-“<br />
ADIV „/“<br />
AMUL „*“<br />
Tabelle 4.4: Tokenklassen-Definitionen für arithmetische Operatoren<br />
INFCONCAT: “||“<br />
PT: “.“<br />
EMPTYSET: “{}“<br />
Für Klammern <strong>und</strong> Kommas werden explizit keine Tokenklassen erzeugt. Sie sind wichtig<br />
für die syntaktische Analyse der Anfrage. Da nach dem Parsing die syntaktische Struktur<br />
der Anfrage im Syntaxbaum gespeichert ist, ist eine Speicherung dieser Zeichen über das<br />
Parsing hinaus nicht notwendig. JavaCC erzeugt für jedes in der Grammatik auftretende<br />
Zeichen automatisch eine anonyme Tokenklasse, die an dieser Stelle völlig ausreicht.<br />
Es bleiben noch die Tokenklassen für Bezeichner <strong>und</strong> Literale zu definieren. Es ist wichtig,<br />
dass sich die Definition der Bezeichner-Klasse unterhalb den Schlüsselwort-Definitionen<br />
befindet, damit ein auftretendes Schlüsselwort auch als solches erkannt wird. Da es sich<br />
bei Bezeichnern in relationenalgebraischen Anfragen um Namen von Tabellen- <strong>und</strong> Spalten<br />
einer Oracle-Datenbank handelt, sind hier die Oracle-spezifischen Namenskonventionen<br />
einzuhalten. Namen von Tabellen <strong>und</strong> Spalten im Oracle-System beginnen mit einen<br />
Buchstaben, sind maximal 30 Zeichen lang <strong>und</strong> können neben großen <strong>und</strong> kleinen Buchstaben<br />
die Zeichen „$“, „_“ <strong>und</strong> „#“ beinhalten. Es ist in JavaCC zwar möglich die<br />
maximale Länge eines Ausdrucks festzulegen, dies würde bei Eingabe eines zu langen Bezeichners<br />
aber dazu führen, dass der Scanner die ersten 30 Zeichen als Bezeichner-Token<br />
erkennt <strong>und</strong> für die restlichen Zeichen ein neues Token erzeugt. Es ist leicht nachvollziehbar,<br />
dass dies beim Parsing zu verwirrenden Fehlermeldungen führen würde. Die Länge<br />
der Bezeichnern soll deshalb erst vom Parser überprüft werden, welcher dadurch in der<br />
17
Lage ist eine aussagekräftige Fehlermeldung auszugeben.<br />
IDENTIFIER: ([A-Z,a-z])([A-Z,a-z] | „$“ | „_“ |„#“)*<br />
Auftretende Literale teilen sich grob in Zeichenketten <strong>und</strong> Zahlen. Beide Gruppen müssen<br />
aber noch genauer differenziert werden. Einige Operationen wie zum Beispiel „substr“ erwarten<br />
ganzzahlige Operanden, während es an vielen anderen Stellen durchaus möglich ist<br />
beliebige Zahlen zu verwenden. Der Parser muss also die Möglichkeit haben beiden Klassen<br />
zu unterscheiden, weshalb der Scanner hier unterschiedliche Tokenklassen bereitstellen<br />
muss.<br />
INTEGER_LITERAL: [1-9][0-9]*<br />
NUM_LITERAL: [0-9]+ “.“ [0-9]+<br />
Gleiches gilt für Zeichenketten. Ein Character-Literal könnte als Zeichenkette betrachtet<br />
werden. Da aber zum Beispiel die Operationen rpad <strong>und</strong> lpad für einen Operand ein einzelnes<br />
Zeichen <strong>und</strong> keine Zeichenkette erwarten, müssen String- <strong>und</strong> Character-Literale<br />
definiert werden. Es wird zunächst ein Hilfsausdruck CHAR definiert, der ein beliebiges<br />
Zeichen annehmen kann. Ein Character-Literal ist dann ein einzelnes CHAR-Zeichen in<br />
Hochkommata, ein String ist eine Aneinanderreihung von CHAR-Zeichen in Hochkommata.<br />
Anmerkung zur Symboltabelle<br />
Der lexikale Scanner speichert zu Beginn implizit die Position eines Tokens in der Eingabemaske,<br />
das Abbild des Tokens als Zeichenkette <strong>und</strong> die Tokenklasse, in der Token-<br />
Datenstruktur. Jedoch arbeitet nur noch der Parser direkt mit den Tokens weiter, so dass<br />
diese Informationen zusätzlich an einer anderen Stelle gespeichert werden müssen. Auch<br />
während der syntaktischen <strong>und</strong> semantischen Analyse werden weitere Informationen gewonnen,<br />
die in irgendeiner Form abgespeichert werden müssen. Hierzu eignet sich zum Teil<br />
der vom Parser erzeugte Syntaxbaum <strong>und</strong> zum Teil eine globale Symboltabelle. Welche<br />
Informationen abzuspeichern sind, <strong>und</strong> ob sich zur Speicherung die Symboltabelle oder<br />
ein Knoten-Objekt des Syntaxbaumes besser eignet, soll an den jeweils geeigneten Stellen<br />
diskutiert werden. Der Aufbau der Symboltabelle wird daher erst nach dem Entwurf der<br />
Analyse-Stufen vorgestellt.<br />
18
4.2.2 Syntaktische Analyse<br />
Für den Parser muss eine LL(1)-Grammatik entworfen werden. Um das eigentliche Parsing<br />
kümmert sich hier wieder der JavaCC-Generator. Die Produktionen sollen hier in Anlehnung<br />
an Javacc in erweiterter Backus-Naur-Form angegeben werden. Token werden dabei<br />
durch die Symbole „“ markiert. Für Klammern wurden keine Token definiert,<br />
weswegen sie einfach als Symbole in den Produktionen vorkommen. Ein nichtterminales<br />
Symbol entspricht in JavaCC einem Methodenaufruf <strong>und</strong> ist daher durch eine zunächst<br />
leere Parameterliste gekennzeichnet.<br />
Entwurf der Grammatik<br />
Ein Ausdruck der Relationenalgebra besteht aus Verschachtelungen von unären <strong>und</strong> binären<br />
Ausdrücken. Eine Produktion der Form<br />
RAExpression(): UnaryRAExpression() | BinaryRAExpression()<br />
würde die LL(1)-Bedingung verletzen, da ein Binärer Ausdruck wieder mit einem Unären<br />
Ausdruck beginnt. Umformuliert kann man sagen, ein Ausdruck der Relationenalgebra<br />
besteht aus einem unären Ausdruck, optional gefolgt von einer binären Operation <strong>und</strong><br />
einem zweiten unären Ausdruck.<br />
RAExpression(): UnaryRAExpression() [ BinaryRAOperation() UnaryRAExpression()]<br />
Es ist hier möglich die Binären Operationen unter einem Nichtterminal zusammenzufassen,<br />
da festgelegt wurde, dass alle binären relationenalgebraischen Operationen die gleiche<br />
Priorität besitzen <strong>und</strong> untereinander geklammert werden müssen. Für die binären Operationen<br />
ergibt sich damit folgende Produktion.<br />
BinaryRAOperation(): |<br />
|<br />
„[„ JoinCondition() „]“ |<br />
„[„ JoinCondition() „]“ |<br />
„[„ JoinCondition() „]“ |<br />
„[„ JoinCondition() „]“ |<br />
„[„ JoinCondition() „]“ |<br />
„[„ JoinCondition() „]“ |<br />
|<br />
|<br />
|<br />
<br />
Ein unärer Ausdruck besteht hingegeben aus einem Tabellennamen, einem geklammerten<br />
Ausdruck mit einer optionalen Umbenennung oder einer unären Operation, also einer<br />
Projektion, einer Selektion oder einer Gruppierung.<br />
19
UnaryRAExpession(): RelationIdent() |<br />
„(„ RAExpression() [ Alias() ] „)“ |<br />
„[„ AttList() „]“ „(„ RAExpression() „)“ |<br />
„[„ Condition() „]“ „(„ RAExpression() „)“ |<br />
„[„ GroupList() „“ AggrList() „]“ „(„ RAExpression() „)“<br />
Für Selektionsbedingung muss ein anderes Nichtterminal verwendet werden als für die<br />
Joinbedingungen, da hier nicht nur Und-Verknüpfungen, sondern beliebige boolsche Verknüpfungen<br />
von Vergleichen erlaubt sind.<br />
Für die Relationen- <strong>und</strong> Aliasnamen ergeben sich folgende Produktionen.<br />
RelationIdent(): <br />
Alias(): <br />
Mit den bisher gegebenen Produktionen lassen sich auf relationenalgebraischer Ebene<br />
bereits beliebige Ausdrücke rekursiv erstellen, es fehlen aber noch die Produktionen für<br />
die Bedingungen <strong>und</strong> Attribut-Listen.<br />
Eine Selektionsbedingung besteht aus logischen Verknüpfungen von Vergleichs-Ausdrücken.<br />
Da die logischen Operatoren unterschiedliche Prioritäten besitzen, müssen hier mehrere<br />
Produktionen erstellt werden. Stellt man sich einen Ableitungsbaum vor, so müssen sich<br />
ganz allgemein Operatoren mit einer niedrigen Priorität möglichst weit entfernt von den<br />
terminalen Operanden-Symbolen, also möglichst weit oben im Baum befinden. Bezogen<br />
auf die Grammatik bedeutet dies, dass für jede Prioritätsstufe eine Produktion vorhanden<br />
sein muss. Die Produktion werden hierarchisch durchlaufen. In der ersten Produktion<br />
können optional Symbole mit niedrieger Priorität erkannt werden oder direkt in die<br />
nächste Produktion gewechselt werden. Bei den logischen Operatoren besitzt die Oder-<br />
Verknüpfung die niedrigste Priorität, eine Und-Verknüpfung eine höhere <strong>und</strong> der Not-<br />
Operator die höchste. Zusäzlich muss die Änderung der Prioritäten durch Klammerung<br />
mit einbezogen werden. Es ergeben sich folgende Produktionen.<br />
Conditon(): AndExpression() ( AndExpression())∗<br />
AndExpression(): NotExpression() (AND NotExpression)∗<br />
NotExpression(): [] ( Comparison() | "‘("‘ Condition() "‘)"’␣)<br />
Da keine Schachtelung von Vergleichen möglich ist, müssen hier keine Prioritäts-Regeln<br />
beachtet werden. Es muss daher nur eine Produktion vorhanden sein.<br />
Comparison(): AttributeTerm() ( AttriuteTerm() |<br />
[] |<br />
|<br />
Literal() Literal() )<br />
Ein Literal kann hierbei ein beliebiges Literal sein.<br />
20
Literal(): StrLiteral() | CharLiteral() | NumLiteral() | IntLiteral()<br />
StrLiteral(): <br />
CharLiteral(): <br />
NumLiteral(): <br />
IntLiteral(): <br />
Für die Attributterme müssen wieder die Prioritäten beachtet werden, weswegen zunächst<br />
eine Produktion für die Addition <strong>und</strong> die Subtraktion benötigt wird <strong>und</strong> eine zusätzliche<br />
für Multiplikation <strong>und</strong> Division.<br />
AttributeTerm(): Term() ( ( | ) Term())*<br />
Term(): Factor() (AND Factor())*<br />
Ein Factor ist entweder ein Attribut, ein geklammerter Ausdruck oder eine Datentyp-<br />
Operation.<br />
Factor():<br />
Attribute() |<br />
“(“ AttributeTerm() “)“ |<br />
( | | ) “(„ StrOperand “)“ |<br />
“(“ StrOperand() “,“ StrOperand() “)“ |<br />
“(“StrOperand() “,“ IntLiteral() “,“ IntLiteral() “)“ |<br />
“(“ StrOpeand() “)“|<br />
“(“ StrOperand() “,“ StrOperand() “)“|<br />
“(“ StrOperand() “,“ StrOperand() “,“ StrOperand() “)“|<br />
( | ) „(„ StrOperand() “,“ IntLiteral() “,“ CharLiteral() “)“|<br />
....|<br />
( | | ) “(“ NumberOperand() “,“ IntLiteral() “)“<br />
( | | | ) “(„ AtomicElement() “,“ AtomicElement() “)“<br />
Bei einem Datentyp-Operand kann es sich jeweils um ein Literal des jeweiligen Typs oder<br />
um ein Attribut handeln.<br />
StrOperand(): StrLiteral() | Attribute()<br />
NumberOperand(): NumLiteral() | IntLiteral() | Attribute()<br />
Ein Attribut ist ein Bezeichner, dem optional ein anderer Bezeichner als Relationennamen-<br />
Prefix, gefolgt von einem Punkt, vorangestellt wird. Dies ist unter Einhaltung der LL(1)-<br />
Bedingung nicht so einfach darstellbar, da der optionale, so wie auch der nicht optionale<br />
Teil mit einem Bezeichner-Token beginnen <strong>und</strong> sich der optionale Teil am Anfang der<br />
Produktion befindet. Der Parser könnte mit nur einem Lookahead-Symbol also nicht wissen,<br />
ob es sich bei dem aktuellen Bezeichner um den Attributnamen oder um einen Präfix<br />
handelt. Abhilfe schafft folgende Produktion.<br />
Attribute(): [ ]<br />
21
Für einen Attribut-Zugriff, der nur über den Attributnamen erfolgt, symbolisiert der<br />
erste Bezeichner den Attributnamen. Für einen Attribut-Zugriff mit Punkt-Notation symbolisiert<br />
der zweite Bezeichner den Attributnamen. Beide Zugriffsmöglichkeiten werden<br />
korrekt geparst.<br />
Auf der Basis der bisherigen Produktionen können nun beliebige Selektionsbedingungen<br />
erkannt werden. Außerdem lässt sich sehr einfach die Produktion für eine Joinbedingung<br />
formulieren. Joinbedingungen sind optional Und-verknüpfte Vergleiche, die Vergleichs-<br />
Produktionen wurden bereits definiert.<br />
JoinContition(): Comparison() ( Comparison() )*<br />
Eine Attributterm-Liste einer Projektion besteht aus mit Komma getrennten Attribut-<br />
Termen <strong>und</strong> jeweils einem opionalem Aliasnamen. Produktionen für Attributterme wurden<br />
bereits definiert.<br />
AttList(): AttributeTerm() [ Alias() ] [“,“ AttList()]<br />
Eine Gruppierungsliste enthält durch Kommas getrennte Attributterme oder das Token<br />
für die leere Menge.<br />
GroupList(): ( AttributeTerm() (“,“ AttributeTerm()())* ) | <br />
Bleibt zuletzt die Aggregierungsterm-Liste von Gruppierungen. Diese enthält aggregierte<br />
<strong>und</strong> unaggregierte Attributterme, jeweils optional gefolgt von einem Alias. Die Aggregierungsfunktion<br />
count kann neben einem Attributterm auch ein “*“ als Operand besitzen.<br />
AggrList(): (AttributeTerm() | AggrTerm()) [Alias()] [“,“ AggrList()]<br />
AggrTerm(): ( | | | ) “(“ AttributeTerm() „)“ | |<br />
„(„ (AttributeTerm() | ) „)“<br />
Die Produktionen der Grammatik können in der JavaCC-Grammatikdatei mit Parser-<br />
Aktionen in Form von Java-Code angereichert werden. Auf diese Weise lassen sich neu<br />
gewonnene Informationen über Tokens direkt in die Symboltabelle eintragen <strong>und</strong> der<br />
Syntaxbaum aufbauen.<br />
Aufbau des Syntaxbaumes<br />
Die naheliegenste Variante wäre es, einen Ableitungsbaum für die eingegebene Anfrage zu<br />
erzeugen, dieses wäre auch automatisch aus Javacc heraus möglich. Nichtterminale Symbole<br />
in einer Grammatik dienen der Beschreibung der Struktur von Worten der Grammatik.<br />
Aber auch ein Syntaxbaum ist eine Datenstruktur zur Speicherung von syntaktischen<br />
Strukturen. Sofern man also im Nachhinein nicht nachvollziehen muss welche Produktionen<br />
der Grammatik in welcher Reihenfolge angewendet wurden, ist es nicht notwendig die<br />
Nichtterminalen Symbole der Grammatik in den Syntaxbaum mit aufzunehmen. Es wäre<br />
sogar nachteilig diese mit aufzunehmen, da zum Einen der entstehende Baum mit Nichtterminalen<br />
Symbolen um ein Vielfaches größer wäre als nur mit terminalen Symbolen <strong>und</strong><br />
zum Anderen ein zusätzlicher Schritt notwendig wäre um aus dem Ableitungsbaum einen<br />
Nichtterminal-freien Baum als Basis für die Optimierung <strong>und</strong> SQL-Code-Erzeugung zu<br />
erzeugen.<br />
22
Aus der Grammatik lässt sich direkt ein Operatorbaum ableiten, also ein Baum, der<br />
die hierarchische Struktur der Operationen in der Anfrage repräsentiert. Im Gegensatz<br />
zum aus der Relationenalgebra bekannten Operatorbaum, müssen hier natürlich auch die<br />
Bedingungen <strong>und</strong> Attributlisten in einer Baumstruktur abgespeichert werden.<br />
Um Ausdrücke der Relationenalgebra repräsentieren zu können sind verschiedene Knoten-<br />
Typen notwendig.<br />
1. Knoten, die nur Operanden-Kindknoten besitzen. Dazu gehören Knoten für die Vereinigung,<br />
den Durchschnitt, die Differenz, die Division, das Kartesische Produkt, den<br />
Natürlichen Verb<strong>und</strong>, sowie alle in Bedingungen auftretende arithmetische Operatoren,<br />
Operatoren für Zeichenketten, boolsche Operatoren <strong>und</strong> Vergleichsoperatoren.<br />
2. Knoten, die zusätzlich zu ihren Operanden-Kindknoten Kindknoten für eine Bedingung<br />
enthalten. Hierzu zählen der Join, der Semijoin, der Antisemijoin, der Outerjoin<br />
<strong>und</strong> die Selektion.<br />
3. Knoten, die Attributterm-Listen abspeichern können müssen. Hierbei muss noch<br />
genauer differenziert werden, da für eine Gruppierung im Gegensatz zur Projektion<br />
zwei Listen abgespeichert werden können müssen.<br />
Beim Parsing kann einfach auf Basis des aktuell gelesen Tokens die Art des zu erzeugenden<br />
Knotens bestimmt werden. Für jedes Token, dass im Operatorbaum gespeichert werden<br />
soll, muss ein Knoten erzeugt <strong>und</strong> dem Baum hinzugefügt werden. Die Hierarchie der<br />
Knoten wird durch die Reihenfolge der angewendeten Produktionen bestimmt. Es wird<br />
ein Zeiger definiert, der immer auf das Knoten-Objekt zeigt, an dem der nächste Knoten<br />
angefügt werden muss. Wird dem Operatorbaum ein Knoten-Objekt hinzugefügt, so wird<br />
der Zeiger darauf gesetzt. Je nach Knotenart wird nun ggf. erst auf den Bedingungs-,<br />
Attributterm-Listen oder Gruppierungs-Listen-Nachfolger gewartet, auf den dann wiederum<br />
der Zeiger gesetzt wird. Dieses Vorgehen entspricht strukturell der Abarbeitung<br />
der Produktionen in der Grammatik. Trifft der Parser auf ein nichtterminales Symbol,<br />
wird vor der weiteren Abarbeitung der aktuellen Produktion, zunächst das nichttermiale<br />
Symbol mit Hilfe einer passenden Produktion verarbeitet. Ist der Parser am Ende einer<br />
Produktion angelangt, die einen vollständigen Ausdruck beschreibt, so ist im Operatorbaum<br />
der zugehörige Teilbaum fertig aufgebaut <strong>und</strong> der aktuell zu bearbeitende Knoten<br />
muss eine Ebene hoch navigiert werden. Parallel navigiert auch der Parser in der Hierarchie<br />
der angewendeten Produktionen eine Ebene hoch <strong>und</strong> setzt dort die Abarbeitung<br />
einer Produktion fort.<br />
Bei diesem Vorgehen kommt noch eine Schwierigkeit dazu, da viele Teilausdrücke Infix-<br />
Operatoren enthalten. Der Parser liest die Tokenfolge von links nach rechts <strong>und</strong> für jedes<br />
Token, dass in den Baum aufgenommen werden soll, wird sofort ein Knoten erzeugt <strong>und</strong><br />
dem Baum hinzugefügt. Es ist daher logisch, dass ein Teilbaum für einen beliebigen Ausdruck,<br />
der linker Operand eines Infix-Operators ist, noch nicht an der richtigen Stelle<br />
hängen kann. Stattdessen befindet er sich an der Position an der normalerweise der Infix-<br />
Operator-Knoten hängen müsste. Wenn der Parser das Token für den Infix-Operator liest,<br />
befindet sich, durch die Produktions-bedingte Navigation im Operatorbaum, der Zeiger<br />
an der richtigen Stelle, also auf dem späteren Vaterknoten des Infix-Operator-Knotens, an<br />
dem im Moment noch der Teilbaum hängt, der den linken Operanden des Infix-Operators<br />
repräsentiert. Der Infix-Operator-Knoten muss nun also zwischen dem aktuellen Knoten<br />
23
<strong>und</strong> dem zuletzt eingefügten Kind-Teilbaum eingefügt werden. Der Zeiger wandert wieder<br />
auf den zuletzt eingefügten Knoten, das heißt auf den Infix-Operator-Knoten. Um<br />
für den Wurzelknoten keine Sonderbehandlung einfügen zu müssen wird zu Beginn des<br />
Parsings ein leerer root-Knoten erzeugt, der nur einen Nachfolger, nämlich den eigentlichen<br />
Wurzelknoten des Baumes, haben soll. Zusammenfassend ist der Aufbau-Prozesses<br />
des Operatorbaumes als Flussdiagramm in Abbildung 4.1 dargestellt. Zusätzlich soll die<br />
Funktionsweise durch folgendes Beispiel verdeutlicht werden. Die eingegebene Anfrage sei<br />
Abbildung 4.1: Aufbau des Operatorbaumes<br />
SEL [A+1
werden. Es existieren die folgenden Produktionen, die in Anlehnung an JavaCC in BNF<br />
angegeben sind. Die Tokenklassen sind durch „“ markiert.<br />
1. Start(): Expr()<br />
2. Expr(): UnaryExpr() [ Expr() #]<br />
3. UnaryExpr(): ( „[„ RelExpr() „]“ „(„ Expr() „)“ #) | ( #)<br />
4. RelExpr(): Term() Term() #<br />
5. Term(): AtomicElement() [„+“ Term() # ]<br />
6. AtomicElement(): ( | ) #<br />
Die #-Symbole markieren die Stellen, an den ein Ausdruck abgeschlossen ist <strong>und</strong> der<br />
Zeiger im Operatorbaum eine Stufe hochgestellt werden muss. Sie stehen hier nur repräsentativ<br />
für entsprechenden JavaCode zur Naviagation im Baum.<br />
Vor Beginn des Parsings wird ein Root-Knoten erzeugt, sowie ein Zeiger, der auf Root<br />
zeigt.<br />
Das erste Token ist SEL. Der Parser wendet also nacheinander Produktion 1, 2 <strong>und</strong> 3 an<br />
<strong>und</strong> verarbeitet nun das SEL-Token. Es wird ein Knoten-Objekt erzeugt, dass neben den<br />
normalen Operanden auch einen Bedingungs-Teilbaum aufnehmen kann. Dieses wird an<br />
die nächste frei Position im aktuellen Knoten, also im root-Knoten gehängt. Der Zeiger<br />
wandert auf den gerade eingefügten Knoten.<br />
Das nächste Token ist „[„. Klammern haben keinen Einfluss auf den Operatorbaum. Der<br />
Parser liest das Token <strong>und</strong> bleibt in Produktion 3. Nun ist das Lookahead-Symbol ein<br />
Bezeichner mit Wert „A“. Der Parser trifft in Produktion 3 auf ein nichtterminales Symbol<br />
<strong>und</strong> kann Produktion 4, 5 <strong>und</strong> 6 anwenden um „A“ zu verarbeiten. Ist „A“ gelesen, so<br />
wird wieder ein Knoten-Objekt erzeugt, dass an die nächste freie Position im aktuellen<br />
Knoten-Objekt gespeichert wird. Das aktuelle Knoten-Objekt ist der Selektions-Knoten,<br />
sein Bedingungsteilbaum muss zuerst aufgebaut werden. Der Zeiger wandert danach wieder<br />
auf den eingefügten Knoten.<br />
25
Das Lookahead ist nun „+“. Der Parser beendet Produktion 6 <strong>und</strong> liest dabei die, hier als<br />
Raute gekennzeichnete Anweisung, den Zeiger eine Ebene hochzusetzen. Der Ausdruck<br />
„A“ wurde komplett verarbeitet.<br />
Das Lookahead ist immernoch „+“. Da der Parser Produktion 6 beendet hat kann er nun<br />
die Bearbeitung von Produktion 5 fortsetzen. Für das „+“-Token wird ein Knoten-Objekt<br />
erzeugt. Da „+“ ein Infix-Operator ist, wird das Knoten-Objekt nicht an die nächste freie<br />
Position, sondern zwischen den aktuellen Knoten <strong>und</strong> den zuletzt eingefügten Teilbaum<br />
gehängt. Der Zeiger wandert wieder mit.<br />
Nun wird eine „1“ gelesen. Der Parser trifft auf das nichtterminale Symbol Term() <strong>und</strong><br />
wendet noch einmal Produktion 5 <strong>und</strong> 6 an, liest dann die „1“ <strong>und</strong> erzeugt ein Knoten-<br />
Objekt, welches an die nächste freie Position des aktuellen Knotens gehängt wird.<br />
26
Das Lookahead ist nun „
Der Parser trifft in Produktion 4 auf das nichtterminale Symbol Term(). Das Lookahead<br />
ist „B“. Es werden nacheinander wieder die Produktionen 5 <strong>und</strong> 6 angewendet. Ein neuer<br />
„B“-Knoten wird angefügt.<br />
Die Produktion 6 kann wieder beendet werden, der Teilausdruck „B“ wurde komplett<br />
verarbeitet, der Zeiger im Baum wird eine Ebene hoch gesetzt.<br />
Da das Lookahead eine Klammer <strong>und</strong> kein „+“ kann auch Produktion 5 beendet werden.<br />
Dieses Mal aber ohne Lesen einer Raute, für den erkannten Teilausdruck „B“ wurde der<br />
Zeiger bereits versetzt. Nun kann die Verarbeitung von Produktion 4 fortgesetzt werden,<br />
aber auch hier ist der Parser bereits am Ende angekommen. Beim Lesen der Raute wird<br />
wieder der Zeiger eine Ebene hoch gesetzt, der Ausdruck „A+1
Die Bearbeitung von Produktion 3 wird fortgesetzt. Hierbei liest der Parser nacheinander<br />
die beiden Token „]“ <strong>und</strong> „)“, die keinen Einfluss auf den Operatorbaum haben. Danach<br />
trifft er auf das Expr()-Nichtterminal. Das Lookahead ist „R“. Es müssen also nach<br />
einander die Produktionen 1, 2 <strong>und</strong> 3 angewendet werden, wobei für UnaryExpr() die<br />
Produktion gewählt wird, deren rechte Seite mit einem Bezeichner beginnt. Für „R“ wird<br />
wieder ein Knoten-Objekt erzeugt. Der Zeiger zeigt zur Zeit auf den Selektions-Knoten.<br />
Da „R“ kein Infix-Operator ist, muss das neue Knoten Objekt an die nächste freie Position<br />
im Selektions-Knoten gehängt werden, also als Operand.<br />
Der Parser beendet Produktion 3 <strong>und</strong> setzt den Zeiger eine Ebene hoch. Der Teilausdruck<br />
„R“ wurde komplett verarbeitet.<br />
29
Das Lookahead ist „)“. Der Parser setzt die Verarbeitung von Produktion 2 durch Beendigung<br />
fort, ohne dabei eine Raute zu lesen. Bei der Weiterverarbeitung von Produktion<br />
3 kann er nun die Klammer lesen. Das neue Lookahead ist eine Vereinigung, also beendet<br />
er Produktion 3 <strong>und</strong> setzt den Zeiger eine Ebene hoch. Der Ausdruck „SEL[A+1
Das Lookahead ist nun der Bezeichner „S“. Um S zu verarbeiten muss der Parser die<br />
Produktionen 2 <strong>und</strong> 3 anwenden. Für „S“ wird ein Knoten-Objekt erzeugt <strong>und</strong> an die<br />
nächste freie Position gehängt. Der Zeiger wandert wieder mit.<br />
Die Produktion 3 wird mit hoch setzen des Zeigers beendet, da der Ausdruck „S“ komplett<br />
verarbeitet wurde.<br />
31
Auch Produktion 2 kann beendet werden. Der Ausdruck „SEL[A+1
Zeichenkette nicht von Bedeutung, da diese nur zur Identifikation der zugehörigen Tokenklasse<br />
diente. Die Übersetzung muss auf Basis der Tokenklasse <strong>und</strong> der in SQL üblichen<br />
Syntax für die jeweilige Operation erfolgen <strong>und</strong> ist daher unabhängig vom Abbild in der<br />
Eingabe. Allgemein hängen sehr viele Entscheidungen, wie ein Element zu Prüfen oder<br />
zu Übersetzen ist, von der jeweiligen Tokenklasse ab. Es macht also Sinn, neben der Position<br />
auch die Tokenklasse in der Baumstruktur zu speichern <strong>und</strong> auf die Aufnahme der<br />
Schlüsselwörter in der Symboltabelle ganz zu verzichten.<br />
Anders ist es bei auftretenden Bezeichnern <strong>und</strong> Literalen. Das Abbild als Zeichenkette<br />
hängt hier nicht von der Tokenklasse ab <strong>und</strong> muss zusätzlich gespeichert werden. Für<br />
auftretende Bezeichner gibt es auch noch mehr Informationen, die abgespeichert werden<br />
müssen. In der Anfrage<br />
PROJ[A Aneu](R)<br />
treten drei Bezeichner auf, die alle unterschiedliche Rollen spielen. Beim ersten Bezeichner<br />
handelt es sich um ein Attribut bzw. einen Spaltennamen, beim zweiten um ein Alias<br />
<strong>und</strong> beim dritten um eine Relation bzw. einen Tabellennamen. Die Symboltabelle sollte<br />
daher in der Lage sein diese „Bezeichner-Typen“ abzuspeichern. Die unterschiedlchen<br />
Bezeichner-Typen werden während des Parsings erkannt <strong>und</strong> können direkt in die Symboltabelle<br />
eingetragen werden. Hierzu müssen lediglich die Produktionen<br />
RelationIdent(): <br />
Alias(): <br />
Attribute(): [ ]<br />
mit passenden Symboltabellen-Methoden-Aufrufen angereichert werden. Für die dritte<br />
Produktion ist dies etwas schwieriger, da es sich beim ersten Bezeichner-Token sowohl um<br />
ein Attribut als auch um eine Relation handeln kann. Für den ersten Bezeichner muss<br />
der Bezeichner-Typ „Relation“ eingetragen werden falls das Lookahead ein Punkt ist,<br />
ansonsten handelt es sich um ein Attribut. Falls der zweite Bezeichner auftritt, handelt<br />
es sich dabei auf jeden Fall um ein Attribut, hier ist also keine Abfrage nötig. Man sieht<br />
an dieser Stelle warum es wichtig war den Punkt explizit als Tokenklasse zu definieren,<br />
ein Vergleich mit einer anonymen Tokenklasse wäre nicht möglich gewesen.<br />
Da während des Parsing alle nötigen Informationen über die Tokens in der Token-<br />
Datenstruktur gespeichert sind, ist es nicht notwendig, dass vor Beginn des Parsings schon<br />
Einträge in der Symboltabelle vorhanden sind. Der Parser kann also die erste Eintragung<br />
eines Bezeichners in die Symboltabelle vornehmen <strong>und</strong> gleichzeitig den Bezeichner-Typ<br />
angeben. Natürlich müsste der Parser dann auch die erste Eintragung der Literale vornehmen.<br />
Hierzu sind die Literal-Produktionen mit entsprechendem Code anzureichern.<br />
Der zugehörige Ausdruck eines Aliasnamens ist nur schwer in der Symboltabelle abspeicherbar.<br />
Die Speicherung des Ausdrucks als Zeichenkette wäre zwar möglich, für die<br />
weitere Verarbeitung aber wenig geeignet. Die Speicherung von Teilbäumen in der Symboltabelle<br />
wäre ebenfalls möglich, würde aber zu vielen red<strong>und</strong>anten Informationen <strong>und</strong><br />
im schlechtesten Fall zu einer enormen Symboltabelle führen. Der Ausdruck sollte also<br />
aus dem Syntaxbaum abgeleitet werden <strong>und</strong> wird nicht in der Symboltabelle gespeichert.<br />
33
4.2.3 Semantische Analyse:<br />
Bezüglich der Ausdrücke der Relationenalgebra hat die semantische Analyse folgende<br />
Aufgaben zu bewältigen.<br />
1. Konsistenz-Prüfung<br />
In der Anfrage auftretende Attribut- <strong>und</strong> Relationennamen müssen sich in der Datenbank<br />
widerspiegeln.<br />
2. Prüfung von Gültigkeitsbereichen<br />
Attribut-, Relationen- <strong>und</strong> Aliasnamen sowie Gruppierungen von Attributen besitzen<br />
innerhalb der Anfrage einen gewissen Gültigkeitsbereich. Dieser muss untersucht<br />
werden, um einen semantisch korrekten Zugriff zu garantieren.<br />
3. Datentyp-Prüfung<br />
(a) Attribut-Ebene<br />
In Selektions- <strong>und</strong> Join-Bedingungen auftretende Operatoren, müssen mit Attributen<br />
<strong>und</strong> Literalen passender Datentypen verwendet werden.<br />
(b) Relationen-Ebene<br />
Operationen auf Relationen-Ebene sind ebenfalls nicht mit beliebigen Operanden<br />
möglich. Voraussetzungen, die an die Schemata der Operanden gestellt<br />
werden, müssen daher ebenfalls geprüft werden.<br />
Wie man leicht sieht wird hier eine Verbindung zum Datenbanksystem notwendig. Hierzu<br />
werden die Login-Daten des Nutzers benötigt, die von der Steuerung der Benutzeroberfläche<br />
abgefragt werden können. Zur Interaktion mit dem Datenbanksystem bietet die<br />
Java-Umgebung das Package java.sql. Sobald die Verbindung steht, können die benötigten<br />
Informationen, also Tabellennamen <strong>und</strong> Spaltennamen, mit den zugehörigen Datentypen,<br />
aller benötigter Tabellen, über die Dictionary-View „all_tab_colums“ abgerufen werden.<br />
Das Ergebnis ist ein ResultSet, aus dem die benötigten Informationen nun gewonnen werden<br />
können. Es wird noch eine Datenstruktur benötigt um die jeweiligen Schemata der<br />
Tabellen abzuspeichern.<br />
Konsistenz-Prüfung<br />
Die Relationennamen der Anfrage, die konsistent zur Datenbank verwendet werden müssen,<br />
<strong>und</strong> deren Attribute mit zugehörigen Datentypen benötigt werden, können von der<br />
Symboltabelle abgefragt werden. Die Existenz der verwendeten Relationennamen in der<br />
Datenbank kann implizit beim Abfragen der benötigten Daten erfolgen. Ist ein Relationenname<br />
nicht als Tabelle in der Datenbank vorhanden, so muss dies dem Nutzer durch<br />
eine Fehlermeldung mitgeteilt werden.<br />
Zugriffe auf Attribute finden an verschiedenen Stellen im Operatorbaum statt <strong>und</strong> können<br />
daher nicht gleich beim Abfragen der Relationen-Daten vom Datenbank-System überprüft<br />
werden. Stattdessen werden alle Attribute einer Relation mit Datentypen in einer<br />
Schema-Datenstruktur für jede auftretende Relation in der Symboltabelle abgespeichert.<br />
Da den Datentypen im Oracle-System Längenangaben mitgegeben sind, müssen diese<br />
der Typinformation der Schema-Datenstruktur ebenfalls mitgegeben <strong>und</strong> später in der<br />
Prüfung einbezogen werden.<br />
34
Schema-Berechnung <strong>und</strong> Typ-Prüfung auf Relationenebene<br />
Um zu Prüfen, ob auf Attribute semantisch korrekt zugegriffen wird, muss das Schema<br />
der Operanden der jeweiligen Operation betrachtet werden. Handelt es sich bei einem<br />
Operanden um einen einfachen Tabellennamen, also einen Blattknoten im Operatorbaum,<br />
so ist das Schema bekannt. Handelt es sich bei dem Operanden aber um einen komplexeren<br />
relationenalgebraischen Ausdruck bzw um einen inneren Knoten im Operatorbaum, so<br />
muss das Schema des Ausdrucks berechnet werden. Für die Anfrage<br />
PROJ [A](R NJOIN S)<br />
mit Schema(R)=(R.A:D1, R.B:D2) <strong>und</strong> Schema(S)=(S.B:D2, S.C:D3) muss zunächst das<br />
Schema des Ausdrucks „R njoin S“ berechnet werden, bevor überprüft werden kann, ob<br />
A darin enthalten ist.<br />
Es gibt in der Relationenalgebra per Definition für jede Operation eine Vorschrift wie das<br />
Ergebnis-Schema aus den Schemata der jeweiligen Operanden berechnet werden kann. Im<br />
Beispiel gilt<br />
Schema(R NJOIN S) = (R.A:D1, B:D2, S.C:D3)<br />
Um für einen beliebigen Knoten ein Schema zu berechnen, müssen die Schemata seiner<br />
Operanden bekannt sein. Die Berechnung erfolgt dabei nach den, in der Definition<br />
der einzelnen relationenalgebraischen Operationen, festgelegten Regeln. Da zu Beginn die<br />
Schemata der in der Anfrage aufgetretenden Tabellen, also den Blattknoten im Operatorbaum,<br />
bekannt sind, kann die Schema-Berechnung Bottom-up durchgeführt werden.<br />
Zusätzlich müssen Umbennenungen von Ausdrücken mit in die Schema-Berechnung einfließen.<br />
Die Umbenennung eines Ausdrucks führt zur Umbennenung aller Relationennamen-Prefixe,<br />
der im jeweiligen Schema enthaltenden Attribute, ohne dass ein späterer<br />
Zugriff auf die ursprünglichen Relationennamen möglich ist. Die Anfrage 1) führt zu<br />
einem Fehler, korrekt wären hingegen Anfrage 2) oder 3)<br />
1. PROJ[R.A]((R NJOIN S) T)<br />
2. PROJ[T.A]((R NJOIN S) T)<br />
3. PROJ[A]((R NJOIN S) T)<br />
Schema((R NJOIN S) T)= (T.A:D1, T.B:D2, T.C:D3)<br />
Per Definition müssen die meisten Operanden von relationenalgebraischen Operationen<br />
gewisse Voraussetzungen bezüglich ihrer Schemata erfüllen, damit die Operation möglich<br />
ist. Bevor also eine Schema-Berechnung an einem Knoten durchgeführt werden kann,<br />
müssen die Vorausseztungen der Schemata der Operanden überprüft werden. Für jede<br />
Vereinigung, jeden Durchschnitt <strong>und</strong> jede Differenz muss geprüft werden, ob die Schemata<br />
der Kindknoten identisch sind. Nach der Prüfung ergibt sich das Ergebnis-Schema direkt<br />
aus den Schemata der Kindknoten. Für jede Projektion, Selektion, Gruppierung <strong>und</strong> für<br />
jede Art von Join muss zunächst geprüft werden, ob Attribute, die in der Bedingung<br />
vorkommen, auch in den Schemata ihrer Kindknoten vorhanden sind. Danach kann das<br />
Ergebnis-Schema entsprechend den in der Definition der Operation festgelegten Regeln<br />
berechnet werden.<br />
35
Prüfung von Gültigkeitsbereichen <strong>und</strong> Datentyp-Prüfung auf Attributebene<br />
Der Zugriff auf Attribute erfolgt in der Bedingung, Attributtermliste oder Gruppierungsliste<br />
einer Operation. Um zu Prüfen, ob auf Attribute semantisch korrekt zugegriffen<br />
wird, muss das Schema der Operanden der jeweiligen Operation betrachtet werden. Hierzu<br />
müssen die Schemata der Operanden zwischengespeichert werden. Die Prüfung der<br />
Attributterme erfolgt Bottom-up. Für die Blattknoten, also die Attribute, kann mittels<br />
den zwischengespeicherten Schemata einfach überprüft werden, ob die ungruppiert im<br />
Schema Attribute existieren, außerdem können die Datentypen ausgelesen werden. Für<br />
die inneren Knoten muss überprüft werden, ob die Operanden passende Datentypen besitzen<br />
<strong>und</strong> der Ergebnis-Typ abgespeichert werden. Hierzu müssen die Typ-Ausdrücke aller<br />
möglichen Operationen definiert sein.<br />
Attribut- <strong>und</strong> Aggregierungstermlisten von Projektionen <strong>und</strong> Gruppierungen müssen<br />
nicht nur überprüft werden, sondern können zusätzlich ein Schema verändern. Aliasnamen<br />
oder Gruppierungen von Attributen müssen daher im zwischengespeicherten Operanden-<br />
Schema gespeichert werden. Natürlich müssen diese dann herangezogen werden um das<br />
Schema des jeweiligen Gruppierungs- oder Projektions-Knotens zu berechnen. Eine Umbenennung<br />
eines Attributes führt zur Umbenennung des Attributes im jeweiligen Schema,<br />
nach der Umbenennung kann nicht mehr auf den ursprünglichen Namen zugegriffen werden.<br />
Die Anfrage<br />
PROJ [A]( PROJ [A Aneu ](R NJOIN S))<br />
führt zu einem Fehler, da zum Zeitpunkt des äußeren Zugriffs auf A kein Attribut namens<br />
A mehr im Schema existiert.<br />
Schema(PROJ[A Aneu](R NJOIN S))=(R.Aneu:D1, B:D2, S.C:D3)<br />
Auch die Gruppierung von Attributen muss im Schema vermerkt werden, da ein späterer<br />
Zugriff auf einzelne Elemente der Gruppe nicht mehr möglich sein darf.<br />
Schema(GROUP[A,B#A,B,C](R NJOIN S))=(group(A:D1, B:D2), S.C:D3)<br />
Für Gruppierungen ist zusätzlich zu Prüfen, ob in der Aggregierungsterm-Liste auftretende<br />
nicht-aggregierte Attribute den gruppierten Attributen entsprechen. Die Gruppierungsliste<br />
muss daher zuerst durchlaufen werden.<br />
Gesamtablauf<br />
Zu Beginn der Prüfung werden die vorhandenen Spaltennamen mit zugehörigen Datentypen<br />
von der Datenbank abgefragt <strong>und</strong> zunächst in die Symboltabelle eingetragen. Danach<br />
wird der Operatorbaum bottom-up. Für jeden Knoten werden folgende Schritte durchgeführt:<br />
1. (a) Ist der Knoten ein Blattknoten, so hole das zugehörige Schema aus der Symboltabelle<br />
<strong>und</strong> speichere es im Knoten.<br />
(b) Ist der Knoten ein Knoten mit Bedingung, so speichere die Schemata der Operanden<br />
zwischen. Durchlaufe die Attributterme bottom-up. Für jeden Blattknoten<br />
prüfe, ob das Attribut in den zwischengespeicherten Schemata ungruppiert<br />
vorhanden ist <strong>und</strong> speichere den Datentyp im Knoten. Für die inneren<br />
Knoten prüfe die Datentypen der Operanden <strong>und</strong> berechne Ergebnis-Typ.<br />
36
(c) Ist der Knoten eine Projektion, so durchlaufe <strong>und</strong> prüfe die Attributterme<br />
genau wie die Attributterme in Bedingungen, aber speichere zusätzlich Umbenennungen<br />
von Attributen im zwischengespeichertem Schema.<br />
(d) Ist der Knoten eine Gruppierung, so durchlaufe zuerst die Gruppierungsliste<br />
<strong>und</strong> speichere eine Gruppierung der aufgetretenden Attribute im zwischengespeichertem<br />
Schema. Durchlaufe dann die Aggregierungsterm-Liste genau wie<br />
die Attributliste einer Projektion, aber prüfe zusätzlich, ob auftretende nichtaggregierte<br />
Attribute den gruppierten Attributen des zwischengespeicherten<br />
Operanden-Schemas entsprechen.<br />
2. Überprüfe die Operations-bedingten Voraussetzungen, die die Schemata der Operanden<br />
erfüllen müssen.<br />
3. Berechne das aktuelle Schema aus den Schemata den zwischengespeicherten Operandenschemata.<br />
Semantische Analyse während des Parsings<br />
Allgemein lässt sich die semantische Analyse oft während des Parsings über eine attributierte<br />
Grammatik durchführen. Die zu prüfenden Eigenschaften, hier also die Datentypen<br />
<strong>und</strong> die Schemata, werden dabei den nichtterminalen Symbolen als Attribute mitgegeben.<br />
Die Vorschriften, wie ein Datentyp- oder ein Schema-Attribut berechnet wird, sowie die<br />
durchzuführenden Prüfungen, werden den Produktionen als semantische Regeln angefügt.<br />
In einer attributierten Grammatik gibt es synthetische Attribute, die, aus der Sicht des<br />
Ableitungsbaumes eines Ausdrucks, nur vom unteren Teilbaum eines Symbols abhängen.<br />
In einem top-down-Parsing-Verfahren, wie in diesem Fall, können sie in einer Produktion<br />
einfach mittels Rückgabewerten von auftretenden Nichtterminalen <strong>und</strong> den semantischen<br />
Regeln, für das nichtterminale Symbol auf der linken Seite berechnet <strong>und</strong> als Rückgabewert<br />
an die nächst höhere Produktion weitergegeben werden.<br />
Daneben gibt es inherite Attribute, dich nicht vom unteren Teilbaum im Ableitungsbaum,<br />
sondern von anderen Symbolen im Baum abhängen. Hängen die inheriten Attribute<br />
nur von im Baum weiter links liegenden Symbolen ab, so sind sie ebenfalls bei einem<br />
top-down-Parsing-Verfahren während des Parsings berechenbar. Da der weiter links<br />
liegende Teil eines Ableitungsbaum immer bereits durchlaufen wurde, sind die nötigen<br />
Attributwerte bereits bekannt <strong>und</strong> können den nichtterminalen Symbolen als Parameter<br />
mitgegeben werden.<br />
Diese Eigenschaft trifft auf die Relationenalgebra jedoch nicht zu. Die Schemata <strong>und</strong> die<br />
Datentypen können zwar synthetisch berechnet werden, aber um Prüfen zu können, ob auf<br />
Attribtnamen-Datentyp-Kombinationen, die in einer Bedingung oder Attributterm-Liste<br />
auftreten, in den Schemata der Operanden vorhanden sind, müssen erst die Schemata<br />
der Operanden bekannt sein. Einer der Operanden befindet sich aber im zugehörigen<br />
Ableitungsbaum weiter rechts. Auf diese Weise wäre eine attributierte Grammatik hier<br />
also nicht möglich.<br />
37
4.2.4 Die Symboltabelle<br />
Nach den vorangegangenen Überlegungen muss die Symboltabelle in der Lage sein Bezeichner<br />
<strong>und</strong> Literale, jeweils mit der Position in der Eingabe <strong>und</strong> dem Abbild als Zeichenkette,<br />
abzuspeichern. Außerdem muss für jeden Bezeichner der Bezeichner-Typ „Relation“,<br />
„Attribut“ oder „Alias“ gespeichert werden können. Für Attribute <strong>und</strong> Literale<br />
ist zusätzlich ein Datentyp, für Relationen ein Relationenschema abzuspeichern.<br />
Position Image IdentType Datatype Schema<br />
[1, 6] A Attribute string -<br />
[1, 8] Aneu Alias - -<br />
[1, 11] B Attribute int -<br />
[1, 13] 3 - int -<br />
[1, 16] R Relation - (R.A:string, R.C:int)<br />
[1, 20] S Relation - (S.B:int, S.D:int)<br />
Tabelle 4.5: Symboltabelle für die Eingabe PROJ[A Aneu, B+3](R NJOIN S)<br />
38
4.3 SQL-Code-Erzeugung<br />
In der SQL-Code-Erzeugung muss nun aus dem vorhandenen Operatorbaum, die SQL-<br />
Anfrage erzeugt werden.<br />
Der Algorithmus zur Erzeugung des SQL-Codes sollte sich nicht auf die Annahme stützen,<br />
dass gewisse syntaktische Strukturen aufgr<strong>und</strong> von durchgeführten Optimierungen<br />
nicht auftreten. Wenn mit Operatorbäumen beliebiger Struktur umgegannen werden kann,<br />
ist es möglich nachträglich beliebige Optimierungsstufen zu ergänzen.<br />
Um eine Anfrage in der Relationenalgebra in SQL zu übersetzen, müssen zunächst die<br />
einzelnen Operationen betrachtet werden. Die Tabelle 4.6 gibt Auskunft darüber, wie sich<br />
einzelne Operationen der Relationenalgebra in SQL-Anfragen abbilden lassen.<br />
Operation SQL-Pendant Beispiel SQL-Code<br />
Selektion Where-Klausel SEL[A>10](R) SELECT *<br />
FROM R<br />
WHERE A>10<br />
Projektion Select-Klausel PROJ[A Aneu, C](R) SELECT A Aneu, C<br />
FROM R<br />
Vereinigung UNION R UNION S SELECT * FROM R<br />
UNION<br />
SELECT * FROM S<br />
Durchschnitt INTERSECT R INTERSECT S analog<br />
Differenz MINUS,<br />
R MINUS S<br />
analog<br />
EXCEPT<br />
Kart. Produkt From-Klausel R CARTESIAN S SELECT *<br />
FROM R,S<br />
Join From-Klausel R JOIN[A=B] S SELECT *<br />
FROM R JOIN S<br />
ON(A=B)<br />
Natural Join From-Klausel R NJOIN S SELECT *<br />
FROM R NATURAL<br />
JOIN S<br />
Semijoin R SJOIN[A=B] S analog<br />
Anti-Semijoin<br />
Gruppierung<br />
Unteranfrage<br />
in Where-Klausel<br />
Group-by-/<br />
Select-Klausel<br />
R ASJOIN[A=B] S SELECT *<br />
FROM R<br />
WHERE NOT EXISTS<br />
( SELECT *<br />
FROM S<br />
WHERE A=B )<br />
GROUP[A#A,avg(B)](R)<br />
Tabelle 4.6: Übersetzung einzelner Operationen in SQL<br />
SELECT A, avg(B)<br />
FROM R<br />
GROUP BY A<br />
Das Prinzip der verschachtelten From-Klauseln<br />
Man könnte die in der Tabelle angegebenen Relationen R <strong>und</strong> S nun statt als Tabellennamen<br />
als Variablen betrachten. Falls R in der Relationenalgebra-Anfrage ein nicht-trivialer<br />
39
Ausdruck ist, wird in der SQL-Anfrage statt einem Tabellennamen eine Unteranfrage<br />
eingetragen. Hierdurch hätte man einen sehr einfachen Algorithmus, der mit beliebig verschachtelten<br />
Relationenalgebra-Ausdrücken umgehen kann. Als Beispiel soll die folgende<br />
Übersetzung betrachtet werden.<br />
Anfrage:<br />
Übersetzung:<br />
PROJ[B,C](SEL[A>1]()R) NJOIN PROJ[C,D](S)<br />
SELECT ∗<br />
FROM (SELECT B,C<br />
FROM (SELECT ∗<br />
FROM R<br />
WHERE A>1))<br />
NATURAL JOIN (SELECT C,D<br />
FROM S)<br />
Das Problem bei diesem Verfahren ist die stark verschachtelte Struktur. Die Anfragen<br />
werden dadurch sehr schlecht lesbar <strong>und</strong> in den meisten Fällen weniger effizient.<br />
Entwurf einer verbesserten Übersetzung<br />
SQL bietet in jedem SQL-Block sechs Klauseln, die für eine Anfrage benutzt werden<br />
können. Bei der oben angegeben Übersetzung der Relationenalgebra-Ausdrücke werden<br />
jedoch nur zwei bis drei Klauseln von einer Operation benutzt <strong>und</strong> jede weitere Operation<br />
erzeugt eine neue Unteranfrage der From-Klausel. Sinnvoller wäre es mit mehreren Operationen<br />
die Klauseln eines SQL-Blocks zu füllen. Hierzu soll folgendes Beispiel betrachtet<br />
werden.<br />
Anfrage:<br />
Übersetzung 1:<br />
Übersetzung 2:<br />
PROJ[B](SEL[A>1](R))<br />
SELECT B<br />
FROM (SELECT ∗<br />
FROM R<br />
WHERE A>1)<br />
SELECT B<br />
FROM R<br />
WHERE A>1<br />
Die zweite Übersetzungs-Variante kommt ohne Unteranfrage aus <strong>und</strong> ist deshalb der ersten<br />
vorzuziehen. Für die beiden Übersetzungen war keine Umformung der ursprünglichen<br />
Anfrage nötig. Da sich in einem SQL-Block mehr als eine Operation unterbringen lässt,<br />
ist keine eindeutige Abbildung eines Relationenalgebra-Ausdrucks auf eine SQL-Anfrage<br />
möglich. Beide Übersetzungen können daher als SQL-seitige Interpretation des selben<br />
Relationenalgebra-Ausdrucks betrachtet werden.<br />
Der gesuchte Algorithmus soll eine Übersetzung liefern die äquivalent zur eingegebenen<br />
Anfrage ist, also nicht auf Umformungen beruht, also nur auf Basis unterschiedlicher<br />
Interpretationen arbeiten.<br />
Geht man nun davon aus, dass die Anfrage nicht weiter umgeformt werden darf, so muss<br />
zum Beispiel die Anfrage<br />
40
R NJOIN PROJ[A,C](S)<br />
eine Unteranfrage enthalten. Es gäbe sonst keine Möglichkeit den Join nur auf den Spalten<br />
A <strong>und</strong> C der Tabelle S auszuführen. Unter gewissen Umständen wäre es zwar möglich die<br />
Projektion nach außen zu ziehen <strong>und</strong> so auf die Unteranfrage zu verzichten, aber nach<br />
obiger Festlegung wäre dies hier nicht erlaubt. Es ergibt sich folgende SQL-Anfrage:<br />
SELECT ∗<br />
FROM R NATURAL JOIN (SELECT A, C<br />
FROM S)<br />
Das Beispiel lässt sich leicht auf ähnliche Situationen übertragen. Ersetzt man die Projektion<br />
durch eine Selektion oder ein Gruppierung, so ist ebenfalls eine Unteranfrage nötig.<br />
In SQL gibt es die Möglichkeit eine Folge von Joins in einer From-Klausel anzugeben, daher<br />
würde das Ersetzen der Projektion durch eine weitere Join-Operation nicht zu einer<br />
Unteranfrage führen. Die Anfrage „R NJOIN (S NJOIN T)“ führt zu der SQL-Anfrage:<br />
SELECT ∗<br />
FROM R NATURAL JOIN S<br />
NATURAL JOIN T<br />
An den bisherigen Beispielen lässt sich gut erkennen, dass die Entscheidung, ob eine Unteranfrage<br />
in der From-Klausel auszuführen ist, nicht nur an der aktuell betrachteten<br />
Operation, sondern an der Kombination von Operation <strong>und</strong> Operand hängt. Man kann<br />
sich also vorstellen, dass der Operatorbaum Knoten für Knoten durchlaufen wird <strong>und</strong> für<br />
jeden Knoten in Abhängigkeit seines Vaterknotens entschieden wird, ob eine Unteranfrage<br />
stattfinden muss. Eine Projektion löst als Operand eines Joins eine Unteranfrage aus,<br />
als Operand einer Selektion aber nicht. Einige andere Operanden des Joins, wie ein anderer<br />
Join oder eine Tabellenname, lösen hingegen keine Unteranfrage aus. Es wäre sehr<br />
aufwendig alle möglichen Kombinationen von Operation <strong>und</strong> Operanden zu untersuchen,<br />
aber es lassen sich gewisse Gesetzmäßigkeiten erkennen.<br />
Die Mengenoperationen<br />
Die Mengenoperationen Vereinigung, Durchschnitt <strong>und</strong> Minus unterscheiden sich von den<br />
übrigen Operationen dadurch, dass sie nicht die Klauseln eines SQL-Blocks füllen, sondern<br />
zwei unterschiedliche SQL-Blöcke verbinden. Es ist daher nachvollziehbar, dass sobald sie<br />
als Operand einer Nicht-Mengenoperation auftreten, eine Art Unteranfrage ausgeführt<br />
werden muss, da die Aufteilung der Teilanfrage in zwei SQL-Blöcke sonst nicht möglich<br />
wäre. Für die Kindknoten dieser Operationen muss hingegen nie eine Unteranfrage in der<br />
From-Klausel ausgeführt werden, da für den linken <strong>und</strong> den rechten Operanden ein neuer<br />
unbefüllter SQL-Block zur Verfügung steht. Die Anfrage PROJ[A]((R NJOIN S) UNION<br />
SEL[B>1](T)) ist somit ohne Umformung nicht ohne Unteranfrage in der From-Klausel<br />
darstellbar. Die zugehörige SQL-Anfrage wäre:<br />
SELECT A<br />
FROM (SELECT ∗<br />
FROM R NATURAL JOIN S<br />
UNION<br />
SELECT ∗<br />
41
FROM T<br />
WHERE B>1)<br />
Die Projektion ist in diesem Beispiel die erste Operation <strong>und</strong> kann daher benutzt werden<br />
um die Select-Klausel des ersten SQL-Blocks zu füllen. Der nächste Knoten im zugehörigen<br />
Operatorbaum wäre die Vereinigung. Da diese hier als Kindknoten einer Nicht-<br />
Mengenoperation auftritt, ist eine Unteranfrage in der From-Klausel nötig. Die Vereinigung<br />
verbindet zwei neue SQL-Blöcke über einen UNION-Operator. Die beiden SQL-<br />
Blöcke werden dann von den Operanden der Vereinigung, also des Joins <strong>und</strong> der Selektion<br />
befüllt. Auch in diesem Beispiel gilt, dass ein nach-innen-ziehen der Projektion sinnvoller<br />
wäre, als die Anfrage direkt zu übersetzen, aber dies ist wieder die Aufgabe der Code-<br />
Optimierung.<br />
Der (Anti-)Semijoin<br />
Ähnlich lassen sich auch die Semijoins <strong>und</strong> Antisemijoins untersuchen. Ein Antisemijoin<br />
ist nicht in einem einzelnen SQL-Block darstellbar, da er eine Unteranfrage in der<br />
Where-Klausel auslöst. Semijoins lassen sich zwar durch Umformung in einem SQL-Block<br />
darstellen, dies soll aber wieder Aufgabe der Codeoptimierung sein. Somit wird auch hier<br />
eine Unteranfrage in der Where-Klausel ausgelöst. Semijoins <strong>und</strong> Antisemijoins können<br />
also wieder gleich behandelt werden. Auftretend als Operand einer Operation, die keinen<br />
neuen SQL-Block zu Verfügung stellt, lösen sie eine Unteranfrage in der From-Klausel<br />
aus. Die Anfrage R NJOIN (S ASJOIN[A=B] T) ist ohne Unteranfrage nicht darstellbar.<br />
Auch als Operand von einstelligen Operationen wird für einen (Anit-)Semijoin eine<br />
Unteranfrage erzeugt, dies kann durch Umformungen wie<br />
PROJ[A](R ASJOIN[A=B] S) = PROJ[A](R) ASJOIN[A=B] S in der Codeoptimierungsstufe vermieden<br />
werden. Für die Operanden eines (Anti-)Semijoins wird jeweils ein SQL-Block<br />
zur Verfügung gestellt, in dem bereits eine Where-Bedingung existiert. Im rechten SQL-<br />
Block ist die Where-Bedingung die Join-Bedingung, im linken besteht sie aus „(NOT)<br />
EXISTS“ <strong>und</strong> einer Unteranfrage. Legt man nun fest, dass durch Operanden auftretende<br />
Where-Bedingungen mit der vorhandenen Where-Bedingung Und-verknüpft werden, so<br />
wird, abgesehen von den Mengenoperationen, für keinen Operanden eine Unteranfrage<br />
in der From-Klausel ausgelöst. Diese Festlegung widerspricht nicht der Voraussetzung,<br />
dass keine Umformungen in der Relationenalgebra vorgenommen werden dürfen, es handelt<br />
sich hier lediglich um eine mögliche Interpretation. Die Anfrage R ASJOIN[A=B] (S<br />
ASJOIN[B=C] T) führt damit zu folgendem SQL-Code.<br />
SELECT ∗<br />
FROM R<br />
WHERE NOT EXISTS<br />
(SELECT ∗<br />
FROM S<br />
WHERE A=B<br />
AND NOT EXISTS<br />
(SELECT ∗<br />
FROM T<br />
WHERE B=C))<br />
42
Die Anfrage wirkt sehr verschachtelt, es handelt sich bei den Verschachtelungen aber um<br />
die unumgänglichen Unteranfragen in der Where-Klausel. Unteranfragen in der From-<br />
Klausel finden nicht statt. Auch ein (Anti-)Semijoin als linker Operand kommt ohne<br />
Unteranfrage in der From-Klausel aus. Die Übersetzung der Anfrage (R ASJOIN[A=B]<br />
S) ASJOIN[B=C] T führt zu folgendem Code.<br />
SELECT ∗<br />
FROM R<br />
WHERE NOT EXISTS<br />
(SELECT ∗<br />
FROM S<br />
WHERE A=B AND NOT EXISTS<br />
(SELECT ∗<br />
FROM T<br />
WHERE B=C))<br />
Der Join, der Natural Join, der Outerjoin <strong>und</strong> das kartesische Produkt<br />
Joins, Natural Joins, Outerjoins <strong>und</strong> Kartesische Produkte können ebenfalls in einer Gruppe<br />
von Operationen zusammengefasst werden. Sie alle sind zweistellige Operationen, die<br />
sich auf die From-Klausel eines SQL-Blocks abbilden lassen. Sie können innerhalb einer<br />
From-Klausel in beliebiger Kombination, beziehungsweise mehrfach auftreten. Das heißt<br />
hintereinander ausgeführte Operationen dieser Gruppe können immer im selben SQL-<br />
Block in der From-Klausel dargestellt werden. Sofern sie als Operand auftreten führen sie<br />
also nie zu einer Unteranfrage, was allerdings nicht ausschließt, dass für ihre Operanden<br />
Unteranfragen ausgeführt werden müssen. Handelt es sich bei den Operanden wieder um<br />
Operationen dieser Gruppe oder um Tabellennamen, muss keine Unteranfrage ausgeführt<br />
werden. Alle anderen Operationen würden hier als Operand zu einer Unteranfrage führen,<br />
da die Unterbringung der jeweiligen Information im aktuellen SQL-Block sich auf das<br />
Ergebnis der Operation <strong>und</strong> nicht nur auf den jeweiligen Operanden beziehen würde.<br />
Die unären Operationen<br />
Eine Projektion füllt die Select-Klausel eines SQL-Blocks, eine Gruppierung die Group-by<br />
<strong>und</strong> die Select-Klausel. Da die Hintereinanderausführung mehrerer Projektion oder Gruppierungen<br />
semantisch nicht das selbe ist wie die Aufnahme in die selbe Select-Klausel,<br />
müssen hierbei Unteranfragen ausgeführt werden. Hintereinander ausgeführte Selektionen<br />
könnten zwar in eine Where-Klausel aufgenommen werden, dies ist aber auch durch<br />
eine Umformung darstellbar, in der die Selektionsbedingungen zweier Selektionen <strong>und</strong>verknüpft<br />
werden. Somit kann in einen SQL-Block eine Projektion oder Gruppierung,<br />
<strong>und</strong> eine Selektion aufgenommen werden, ansonsten ist eine Unteranfrage auszuführen.<br />
Im Zusammenhang mit den anderen Operationen wurden die einstelligen Operationen<br />
bereits diskutiert.<br />
Die Übersetzung<br />
Zur Übersetzung wird der gegebene Operatorbaum durchlaufen, wobei die Knoten unterschiedlichen<br />
SQL-Block-Objekten zugeordnet werden. Ein SQL-Block enthält neben<br />
Varibalen für die üblichen Klauseln eine zweite Where-Klausel für (Anti-)Semijoins. Bis<br />
43
auf die From-Klausel darf jede Klausel nur einmal befüllt werden. Das Zusammenfügen<br />
der Where-Klauseln geschieht beim Auslesen der Anfrage aus dem Block-Objekt. Da neben<br />
normalen SQL-Blöcken auch Mengenoperationen möglich sind, müssen auch Objekte<br />
einer Mengenoperationsklasse erzeugt werden können, die jeweils zwei Operanden <strong>und</strong><br />
eine Operation aufnehmen können.<br />
Man kann sich gut vorstellen, dass Operationen, die sich nach der Übersetzung in einem<br />
SQL-Block befinden, im Operatorbaum zusammenhängend sein müssen. Wird an einer<br />
Stelle im Operatorbaum festgestellt, dass für einen Operanden eine Unteranfrage erzeugt<br />
werden muss, so befinden sich alle Operationen, die die Klauseln der Unteranfrage füllen,<br />
im Teilbaum, der den betrachteten Operand als Wurzel besitzt. Die Operationen innerhalb<br />
dieses Teilbaumes können entweder den SQL-Block der Unteranfrage füllen, oder wieder<br />
neue Unteranfragen erzeugen; sie können nicht (direkt) die Klauseln des äußeren SQL-<br />
Blocks füllen.<br />
Der Baum muss nun durchlaufen <strong>und</strong> nach den Knoten durchsucht werden, an denen<br />
neue SQL-Blöcke beginnen. Dies wird immer aus Sicht der Operation für die jeweiligen<br />
Operanden entschieden, da für Operationen der Mengenoperations- <strong>und</strong> der Semijoin-<br />
Gruppe unabhängig von den Operanden neue SQL-Blöcke erzeugt werden müssen <strong>und</strong> es<br />
von der Operation abhängt, ob die verschiedene SQL-Blöcke über eine Mengenoperation,<br />
über die Where-Klausel oder über die From-Klausel verb<strong>und</strong>en werden.<br />
Es soll zunächst beispielhaft der union-Knoten aus dem Beispiel-Baum 4.2 betrachtet<br />
werden. Für seine Operanden müssen immer neue SQL-Blöcke erzeugt werden. Bei einem<br />
rekursiven Baum-Durchlauf könnte auf der union-Ebene also für den linken Operanden<br />
ein neuer SQL-Block erzeugt werden, der dann rekursiv den linken Teilbaum des union-<br />
Knotens durchläuft <strong>und</strong> befüllt wird. Bei der Rückgabe des Blocks an die union-Ebene ist<br />
dieser nach den obigen Überlegungen komplett. Er könnte also bereits als Zeichenkette<br />
ausgelesen werden <strong>und</strong> im Mengenoperations-Block untergebracht werden. Der linke Teilbaum<br />
wäre damit abgearbeitet <strong>und</strong> der als Zeichenkette ausgelesene SQL-Block verhält<br />
sich im weiteren nicht anders wie eine Zeichenkette für einen Tabellennamen.<br />
Auf dieser Basis kann eine Übersetzungs-Methode definiert werden, die rekursiv für jeden<br />
Knoten mit einem Block-Objekt block1 aufgerufen wird <strong>und</strong> folgender Weise vorgeht:<br />
1. Knoten=Mengenoperation: Rufe Operanden-Ebenen mit neuen SQL-Blöcken auf,<br />
lese die Blöcke als Strings aus <strong>und</strong> führe diese in einem Mengenoperations-Block<br />
zusammen.<br />
2. Knoten=(Anti-)Semijoin: Rufe linke Operanden-Ebene mit übergebenen Block-Element<br />
block1 auf. Rufe dann rechte Operanden-Ebene mit neuem Block-Element block2<br />
auf, lese block2 als String aus <strong>und</strong> speichere ihn mit (NOT) EXISTS in der Where-<br />
Klausel von block1.<br />
3. Knoten=Joingruppen-Element: Rufe erst erste Operanden-Ebene auf, speichere dann<br />
Join-Operator als Zeichenkette in der From-Klausel von block1 <strong>und</strong> rufe die zweite<br />
Operanden-Ebene auf.<br />
(a) Für Operanden aus Semijoin-, Mengenoperations- oder unärer Gruppe, rufe<br />
Operanden-Ebene mit neuem Block-Element block2 auf, lese block2 als String<br />
aus, füge zwei Klammern hinzu <strong>und</strong> speichere String in der From-Klausel von<br />
block1<br />
(b) Sonst: Rufe Operanden-Ebene mit block1 auf.<br />
44
4. Knoten=Element der unären Gruppe: Lese Selektionsbedingung bzw. Attribut- oder<br />
Gruppierungslisten in entsprechende Klauseln von block1 ein. Falls diese bereits<br />
belegt: Erzeuge neues Block-Element block2<br />
(a) Falls Operand aus Semijoin- oder Mengenoperations-Gruppe, rufe Operanden-<br />
Ebene mit neuem Block-Element block3 auf. Füge block3 als String mit zusätzlichen<br />
Klammern in der From-Klausel von, falls vorhanden block2, sonst<br />
block1, ein.<br />
(b) Sonst: rufe Operanden-Ebene mit, falls vorhanden block2, sonst block1, auf.<br />
Gegebenenfalls wird block2 als String mit Klammern in block1 eingetragen.<br />
5. Falls Operand=Tabellenname: Trage Tabellennamen in die From-Klausel von block1<br />
ein.<br />
Zur besseren Verdeutlichung soll das Verfahren im Folgenden durch die Übersetzung des<br />
Beispielbaumes 4.2 angewendet werden.<br />
Übersetzung des Beispielbaumes<br />
1.Aufruf<br />
Njoin1-Ebene<br />
Union-Ebene<br />
Proj-Ebene<br />
Njoin2-Ebene<br />
R-Ebene<br />
Njoin2-Ebene<br />
S-Ebene<br />
Njoin2-Ebene<br />
Proj-Ebene<br />
Der Aufruf des Wurzelknoten-Ebene erfolgt mit einem leeren SQL-Block-<br />
Objekt. (hier: block1)<br />
Da es sich beim linken Operanden um eine Mengen-Operation handelt,<br />
wird für union ein neues Mengen-Opearations-Objekt erzeugt.(hier: mengenBlock1)<br />
Für Nachfolger von Mengenoperationen wird immer ein neuer SQL-Block<br />
Erzeugt, daher wird die Projektions-Ebene wieder mit einem neuen Block<br />
aufgerufen. (hier: block2)<br />
Aus der Attributliste der Projektion wird eine Zeichenkette erstellt <strong>und</strong> in<br />
die Select-Klausel von block2 eingetragen. Für ein Joingruppen-Element<br />
als Operand eines Elements der unären Gruppe wird keine Unteranfrage<br />
erzeugt. Daher wird die njoin-Ebene mit block2 aufgerufen.<br />
Tabellennamen als Operanden führen nie zu Unteranfragen, daher wird<br />
die R-Ebene mit block2 aufgerufen.<br />
„R“ wird an die From-Klausel von block2 angehängt, die R-Ebene ist abgearbeitet,<br />
block2 wird zurück gegeben.<br />
In der Njoin-Ebene wird nun „NATURAL JOIN“ an die From-Klausel von<br />
block2 gehängt. Da der linke Operand keine Unteranfrage erzeugt, wird<br />
die S-Ebene mit block2 aufgerufen.<br />
„S“ wird an die From-Klausel von block2 angehängt, die S-Ebene ist abgearbeitet,<br />
block2 wird zurück gegeben.<br />
Die njoin-Ebene ist abgearbeitet block2 wird zurück gegeben.<br />
Die Proj-Ebene ist abgearbeitet, block2 wird zurückgegeben.<br />
45
Abbildung 4.2: Beispiel-Baum für die Code-Erzeugnung<br />
Union-Ebene<br />
Die linke Teilanfrage der Vereinigung ist komplett <strong>und</strong> kann als String<br />
„Select A From R NATURL JOIN S“<br />
für den linken Operanden von mengenBlock1 eingetragen werden. Die<br />
Mengenoperation in mengenBlock1 wird auf „UNION“ gesetzt. Für den<br />
rechten Operand einer Mengenoperation wird wie für den linken ein neuer<br />
SQL-Block übergeben. (hier block3)<br />
T-Ebene<br />
Union-Ebene<br />
„T“ wird an die From-Klausel von block3 angehängt, die S-Ebene ist abgearbeitet,<br />
block3 wird zurück gegeben.<br />
Die rechte Teilanfrage der Vereinigung ist komplett <strong>und</strong> kann als String<br />
„Select * From T“ 1<br />
1 Dass für eine unbefüllte Select-Klausel „Select *“ ausgeben werden muss, muss in der toString-<br />
Methode der SQL-Block-Klasse beachtet werden<br />
46
für den rechten Operanden von mengenBlock1 eingetragen werden. Die<br />
union-Ebene ist abgearbeitet <strong>und</strong> mengenBlock1 kann zurückgegeben werden.<br />
Njoin1-Ebene<br />
In der Njoin-Ebene wird mengenBlock1 als Zeichenkette mit zusätzlichen<br />
Klammern in die From-Klausel von block1 aufgenommen. Die Zeichenkette<br />
dazu lautet nun<br />
„(Select A From R NATURL JOIN S<br />
UNION<br />
Select * From T)“<br />
Danach wird „NATURAL JOIN“ angehängt. Der rechte Operand des<br />
njoin-Knotens ist eine unäre Operation <strong>und</strong> löst als Operand eines Elementes<br />
der Join-Gruppe eine Unteranfrage aus. Die Sel-Ebene wird also<br />
mit einem neuem Block-Element block4 aufgerufen.<br />
Sel-Ebene<br />
U-Ebene<br />
Sel-Ebene<br />
Njoin1-Ebene<br />
Der Bedinungs-Teilbaum der Selektion wird als String ausgelesen <strong>und</strong> in<br />
die Where-Klausel von block4 eingetragen. Da Tabellennamen als Operand<br />
von unären Operationen keine Unteranfrage auslösen, wird die U-Ebene<br />
mit block4 aufgerufen.<br />
„U“ wird an die From-Klausel von block4 angehängt, die U-Ebene ist<br />
abgearbeitet, block4 wird zurück gegeben.<br />
Die Sel-Ebene ist abgearbeitet, block4 wird zurückgegeben.<br />
Da block4 für eine From-Unteranfrage erzeugt wurde, wird block4 als<br />
Zeichenkette mit zusätzlichen Klammern<br />
„( Select * FROM U WHERE A>1 )“<br />
in die From-Klausel von block1 eingetragen. Block1 wird zurückgegeben<br />
<strong>und</strong> enthält die gesamte Anfrage<br />
Select ∗<br />
From (Select A From R NATURL JOIN S<br />
UNION<br />
Select ∗ From T)<br />
NATURAL JOIN<br />
(Select ∗ From U Where A>1)<br />
47
Kapitel 5<br />
Implementierung<br />
5.1 Package-Struktur<br />
Das Projekt gliedert sich in zwei Packages<br />
1. de.ra2sql.core<br />
2. de.ra2sql.gui<br />
Das core-Package enthält alle Klassen, die zur internen Verarbeitung der Anfrage nötig<br />
sind. Hierzu gehören die Symboltabelle, der Scanner, der Parser, die Semantische-Analyse-<br />
Einheit, die SQL-Code-Erzeugung, eine Klasse, die die Datanbank-Anfragen regelt, die<br />
TreeBuilder-Klasse zum Aufbau des Operatorbaumes <strong>und</strong> diverse Datenstrukturen zur<br />
Speicherung von Informationen wie zum Beispiel Datenstrukturen für die Knoten-Objekte<br />
<strong>und</strong> die Schema-Datenstruktur.<br />
Die Haupt-Komponenten Parser, Semantische-Analyse-Einheit <strong>und</strong> Code-Erzeugung arbeiten<br />
unabhängig voneinander. Sie müssen von außen mit einem zu verarbeitenden Input<br />
aufgerufen werden <strong>und</strong> geben der aufrufenden Klasse den verarbeiteten Output zurück.<br />
Das gui-Package enthält die Klassen für die Graphische Benutzerschnittstelle. Die Haupt-<br />
Klasse GUI ist für die Visualisierung der Benutzerschnittstelle sowie für die Ansteuerung<br />
der Haupt-Komponenten des core-Packages zuständig.<br />
5.2 Die Grammatikdatei <strong>und</strong> generierte Klassen<br />
Die Grammatikdatei besteht aus einem Optionen-Block, einem Parser-Block, der zusätzlichen<br />
Code für den generierten Parser enthalten kann, den Token-Definitionen <strong>und</strong> den<br />
Produktionen der Grammatik.<br />
Da die zu entwickelnde Anfrage-Sprache nicht case-sensitive sein soll, ist im Optionenblock<br />
die Option „IGNORE_CASE“ zu setzen.<br />
Der Parser-Block enthält neben der Package-Angabe eine statische Instanz der Klasse<br />
TreeBuilder, zum Aufbau des Operatorbaumes, <strong>und</strong> eine parse-Methode. Die Method erwartet<br />
die zu parsende Anfrage in einem java.io.Reader-Objekt <strong>und</strong> gibt den Wurzelknoten<br />
des erzeugten Operatorbaumes zurück. Ihre Aufgaben bestehen darin, alle verwendeten<br />
statischen Klassen <strong>und</strong> Instanzen zu reinitialisieren, sich selbst <strong>und</strong> damit auch implizit<br />
48
den Scanner mit der übergebenen Anfrage zu reinitialisieren, das Start-Symbol der Grammatik<br />
aufzurufen <strong>und</strong> den Wurzelknoten des Operatorbaumes beim TreeBuilder-Objekt<br />
abzufragen <strong>und</strong> dem Aufrufer zurück zu liefern.<br />
Die Token-Definitionen <strong>und</strong> die Grammatik können Kapitel 4.2.1 <strong>und</strong> 4.2.2 entnommen<br />
werden. Zusätzlich wird innerhalb der Produktionen der Grammatik der Tree-Builder<br />
angesteuert <strong>und</strong> Relationen, Aliase <strong>und</strong> Literale in die Symboltabelle aufgenommen.<br />
Die generierten Klassen<br />
Aus der Grammatik-Datei werden sieben Klassen generiert. Die Klassen<br />
• SimpleCharStream.java<br />
• Token.java<br />
• TokenMgrError.java<br />
• ParseException.java<br />
werden nur generiert sofern sie nicht existieren, da sie unabhängig vom Inhalt der Grammatikdatei<br />
sind. Außerdem werden die Klassen<br />
• Parser.java<br />
• ParserConstants.java<br />
• ParserTokenManger.java<br />
erzeugt. ParserConstants enthält die Konstanten für die in der Grammatikdatei definierten<br />
Tokenklassen. Der ParserTokenManager stellt den lexikalen Scanner dar, der anhand<br />
der in der Grammatikdatei definierten regulären Ausdrücke für die Tokenklassen, aus dem<br />
SimpleCharStream, der die Eingabe enthält, eine Tokenfolge erstellt. Der Scanner muss<br />
nicht expilzit aufgerufen werden, sondern wird automatisch durch den generierten Parser<br />
angesteuert. Der Parser enthält für jede Produktion der Grammatik eine eigene Methode,<br />
die eine ParseException werfen kann. Das aktuelle Token-Objekt ist in der Variable token<br />
gespeichert, das Lookahead kann über die Methode jj_ntk abgefragt werden. Der Parser<br />
ist statisch <strong>und</strong> muss daher über eine ReInit()-Methode reinitialisert werden, welche<br />
automatisch auch den TokenManager reintialisiert.<br />
5.3 Beschreibung der nicht generierten Klassen<br />
Die Knoten-Datenstrukturen<br />
Die Knoten-Datenstrukturen werden für den Operatorbaum benötigt. Von allen Knoten-<br />
Objekten gespeichert werden müssen die Operanden, die ebenfalls wieder Knoten-Objekte<br />
sind, der Vaterknoten, die Tokenklasse, die Position zur Identifizierung eines Symboltabellen-<br />
Eintrags <strong>und</strong> zur Ermöglichung einer gezielten Fehler-Ausgabe, ein Relationenschema <strong>und</strong><br />
ein Datentyp.<br />
Die benötigten Getter- <strong>und</strong> Setter- Methoden werden im Interface INode spezifiziert.<br />
Die Klasse Node implementiert das Interface INode ohne zusätzliche Funktionalität. Alle<br />
Elemente der Anfrage, die keine Attributliste oder Bedingung beinhalten, können durch<br />
49
Node-Objekte dargestellt werden. Hierzu gehören alle Elemente innerhalb einer Attributliste<br />
oder einer Bedingung, alle Mengenoperationen, das kartesische Produkt, der natürliche<br />
Verb<strong>und</strong>, so wie alle Bezeichner.<br />
Die Knoten-Datenstruktur zur Speicherung von Operationen mit Bedingung, also die<br />
Selektion <strong>und</strong> die Joins, erweitert die Klasse Node um die Speicherung eines Bedingungs-<br />
Knotens <strong>und</strong> die zugehörigen Getter <strong>und</strong> Setter.<br />
Die Knoten-Datenstruktur zur Speicherung von Operationen mit Attributliste erweitert<br />
die Node-Klasse um die Speicherung eines Arrays, welches die einzelnen Wurzelknoten<br />
der Attributterm-Listenelemente enthält, sowie ebenfalls um die Getter- <strong>und</strong> Setter-<br />
Methoden.<br />
Da die Gruppierung zwei Listen enthält, erweitert die zugehörige Knoten-Datenstruktur<br />
die Listen-Knoten-Klasse um eine zweite Liste, die Gruppierungsliste, sowie um die nötigen<br />
Getter <strong>und</strong> Setter für die Gruppenelemente.<br />
Das Klassendiagramm 5.1 soll die Knoten-Hierarchie verdeutlichen.<br />
SymbolTableEntry<br />
Objekte der Klasse SymbolTableEntry stellen Symboltabellen-Einträge dar. Sie speichern<br />
jeweils die Position, das Abbild, den Bezeichner-Typ, ein Relationen-Schema <strong>und</strong> einen<br />
Datentyp. Außerdem werden die nötigen Getter- <strong>und</strong> Setter-Methoden bereitgestellt.<br />
SymbolTable<br />
Die Klasse SymbolTable enthält eine Liste von Symboltabellen-Einträgen der Klasse SymbolTableEntry.<br />
Die Methoden setAttribute, setAlias, setRelation <strong>und</strong> setLiteral werden<br />
vom Parser mit einem Token-Parameter aufgerufen <strong>und</strong> legen jeweils ein neues Eintrag-<br />
Objekt mit bekannter Position, bekanntem Abbild <strong>und</strong> Bezeichner-Typ an. Die Bezeichner-<br />
Typen sind als Konstanten definiert. Neben den bereits genannten Methoden <strong>und</strong> den noch<br />
fehlenden Gettern <strong>und</strong> Settern, wird noch ein Methode benötigt, die alle unterschiedlichen<br />
Relationennamen der Tabelle zurück gibt. Diese wird benötigt um die Schemata<br />
aller Relationen aus der Datenbank abzufragen. Alle angebotenen Methoden, sowie auch<br />
die Liste mit Einträgen, werden statisch implementiert, daher wird noch eine Methode<br />
ReInit() benötigt, die die Einträge der Symboltabelle entfernt um diese auf die nächste<br />
Anfrage vorzubereiten.<br />
TreeBuilder<br />
Die TreeBuilder-Klasse wird vom Parser angesteuert <strong>und</strong> ist für den Aufbau des Operatorbaumes<br />
verantwortlich.<br />
Der TreeBuilder speichert global den root-Knoten, an welchem der Wurzel-Knoten des<br />
eigentlichen Operatorbaumes angehängt werden soll. Außerdem muss der Zeiger auf den<br />
aktuell zu bearbeitenden Knoten global gespeichert werden. Die, durch den Parser gesteuerte,<br />
Navigation im Baum geschieht durch die Methoden add <strong>und</strong> up. Ob es sich bei<br />
einem Token um eine Infix-Operation handelt, entscheidet der TreeBuilder intern. Da für<br />
das An- oder Umhängen des ersten Knotens einer Attributliste, einer Gruppierungsliste<br />
oder einer Bedingung spezielle Zugriffsmethoden benutzt werden müssen, muss der Tree-<br />
Builder wissen, wann dies der Fall ist. Hierzu werden vier Modi als statische Konstanten<br />
definiert <strong>und</strong> eine Variable, die die Position des Zeigers, als Tiefe innerhalb des aktuellen<br />
50
Abbildung 5.1: Klassenhierarchie der Knoten-Objekte<br />
Bedingungs-, Attributterms- oder Gruppierungslisten-Teilbaums angibt. Eine besondere<br />
Zugriffsmethode, ist immer dann nötig, wenn der Modus nicht auf Normal steht <strong>und</strong> die<br />
Tiefe Null ist. Die Art des gesonderten Zugriffs hängt vom Modus ab. Dieser wird vom<br />
Parser in den jeweiligen Produktionen gesetzt.<br />
51
Abbildung 5.2: Die Klasse TreeBuilder<br />
Attribute<br />
Ein Attribut speichert einen Attributnamen, einen (optional leeren) Relationennamen-<br />
Präfix, <strong>und</strong> einen Datentyp. Es werden die nötigen Getter- <strong>und</strong> Setter-Methoden bereitgestellt,<br />
sowie eine equals-Methode <strong>und</strong> eine isCompatibleTo-Methode bereitgestellt. Zwei<br />
Attribute sind gleich, wenn sie den gleichen Namen, den gleichen Präfix <strong>und</strong> den gleichen<br />
Datentyp besitzen. Sie sind kompatibel, wenn Name <strong>und</strong> Datentyp übereinstimmen. Die<br />
Präfixe dürfen sich dabei also unterscheiden. Die Methode wird an mehreren Stellen von<br />
der Semantischen Analyse benötigt. Zum Beispiel müssen für eine Vereinigung zweier<br />
Relationen, alle Attribute der Schemata kompatibel sein.<br />
RelationsSchema<br />
Ein Relationenschema-Datenstruktur muss eine Liste von Attributen speichern können.<br />
Außerdem müssen neben den üblichen Getter- <strong>und</strong> Setter-Methoden, Methoden bereitgestellt<br />
werden, die ein Schema-Objekt mit einem gegebenen Schema-Objekt auf Gleichheit,<br />
Teilmengen-Beziehungen, oder Kompatiblität überprüfen. Zwei Schema-Objekte sind<br />
kompatibel, wenn sie gleich viele Attribute besitzen <strong>und</strong> diese zueinander kompatibel<br />
sind. Zusätzlich beinhaltet die Schema-Datenstuktur eine toString-Methode, die es möglich<br />
macht bei einem semantischen Fehler involvierte Schemata mit auf dem Bildschirm<br />
auszugeben.<br />
SchemaOperations<br />
Die Klasse SchemaOperations bietet statische Methoden zur Berechnung von Schemata<br />
an.<br />
Semantische Analyse<br />
Die Klasse SemanticalAnalysisUnit implementiert die semantische Prüfung der Schemata<br />
<strong>und</strong> Datentypen im Operatorbaum.<br />
52
Sie wird über die öffentliche Methode „analyse“ aufgerufen, die als Paramenter den Wurzelknoten<br />
des zu Prüfenden Operatorbaumes erwartet <strong>und</strong> den Wurzelknoten des überarbeitenden<br />
Baumes zurückgibt. Im Fehlerfall wird eine SemanticalException geworfen.<br />
Bevor mit der Analyse begonnen wird, wird die Methode „readOutRelationsSchemata“<br />
der Klasse DatabaseFrontend aufgerufen, welche die Schemata aller in der Anfrage verwendeten<br />
Relationennamen von der Datenbank abfragt <strong>und</strong> in die Symboltabelle einträgt.<br />
Die Analyse erfolgt rekursiv über die Methode „recursiveAnalyse“. Für die Blattknoten<br />
werden die jeweiligen Schemata aus der Symboltabelle abgefragt <strong>und</strong> im Knoten-Objekt<br />
eingetragen. Ist in der Symboltabelle für einen Blattknoten kein Schema-Eintrag vorhanden,<br />
so wird eine SemanticalException geworfen. Die Schemata der inneren Knoten werden<br />
nach den jeweiligen Prüfungen über die Methode computeSchema berechnet, die zur<br />
Berechnung die statischen Methoden der Klasse SchemaOperations nutzt. Für jedes von<br />
Node abgeleitete Knoten-Objekt, werden die Schemata des linken <strong>und</strong> des rechten Operanden<br />
in den globalen Schema-Variablen currentSchemaLeft <strong>und</strong> currentSchemaRight<br />
abgespeichert <strong>und</strong> je nach Knotentyp die passende Methode zur Prüfung der Bedingung,<br />
der Attributliste oder der Gruppierungsliste aufgerufen.<br />
Abbildung 5.3: Klassendiagramm für die Semantische Analyse<br />
SQLBlock<br />
Die Klasse SQLBlock wird für die SQL-Code-Erzeugung benötigt. Sie bietet die Methoden<br />
setSelect, addToFrom, setWhere, setWhereExists, setGroupBy, setHaving <strong>und</strong> toString<br />
an. Die Methode set-Methoden speichert den übergebenen String in der globalen Variable<br />
falls dieser zuvor null war <strong>und</strong> gibt true zurück. Ist die Variable bereits initialisiert, so<br />
wird false zurückgegeben. Auf diese Weise weiß die aufrufende ConversionUnit, ob die<br />
Speicherung einer Klausel im aktuellen SQL-Block möglich ist oder ob ein neuer Block<br />
53
erzeugt werden muss. Die Methode addToFrom <strong>und</strong> fügt den übergebenen String der<br />
globalen From-Liste hinzu.<br />
Operations<br />
Die Klasse Operations beinhaltet die statischen Konstanten für die Einteilung der relationenalgebraischen<br />
Operationen in Gruppen, sowie eine statische Methode zur Zuordnung<br />
der Gruppenvariablen zu den einzelnen Operationen. Sie erwartet eine Konstante für die<br />
Tokenklasse <strong>und</strong> gibt eine Konstante für die Gruppe zurück.<br />
ConversionUnit<br />
Die Klasse ConversionUnit implementiert die Übersetzung der Anfrage. Die Klasse bietet<br />
eine öffentliche statische decodeToSQL-Methode an, die den Wurzelknoten des Operatorbaumes<br />
als Parameter erwartet <strong>und</strong> die SQL-Anfrage als String zurückgibt. Die Übersetzung<br />
findet dann rekursiv in der privaten Methode „decode“ statt. Diese erwartet ein<br />
Objekt der Klasse SQL-Block, das aktuelle Knoten-Objekt <strong>und</strong> einen String, der Leerzeichen<br />
oder Tabulatoren zur Formatierung der SQL-Anfrage enthalten kann. Die Zuordnung<br />
der Gruppen-Variablen zu den verschiedenen Relationenalgebraischen Operationen erfolgt<br />
über die statischen Methoden der Klasse „Operations“. Die Übersetzung der Bedingungen,<br />
Attributlisten <strong>und</strong> Gruppierungslisten erfolgt wieder in eigenen Methoden.<br />
Abbildung 5.4: Klassendiagramm für die Code-Erzeugnunge<br />
54
DatabaseFrontend<br />
Die Klasse DatabaseFrontend ist für die Datenbank-Anfragen zuständig. Sie beinhaltet<br />
die Methoden initConnection, readOutRelationsSchemas, executeSQLQuery <strong>und</strong> close-<br />
Connection.<br />
Die Methode initConnection erwartet einen Benutzernamen <strong>und</strong> ein Passwort <strong>und</strong> baut<br />
die Verbindung zur Datenbank auf. ExecuteQuery erwartet eine SQL-Anfrage als String,<br />
führt diese aus <strong>und</strong> gibt einen java.io.Reader zurück, der die formatierte Ergebnis-Tabelle<br />
enthält. Die Methode readOutRelationsSchemas fragt alle in der Anfrage aufgetretenden<br />
Relationennamen über die Methode getDistinctRelationEntries von der Symboltabelle ab,<br />
fragt alle Attribut-Datentyp-Paare von der Datenbank ab, erzeugt daraus Schema-Objekte<br />
<strong>und</strong> speichert diese in der Symboltabelle.<br />
55
Kapitel 6<br />
Die Benutzerschnittstelle<br />
Die Benutzerschnittstelle besteht im Wesentlichen aus einem Eingabe- <strong>und</strong> zwei Ausgabe-<br />
Bereichen, zwei Toolbars <strong>und</strong> einer Menüzeile.<br />
Abbildung 6.1: Die Benutzer-Schnittstelle<br />
Die Eingabe<br />
Die Eingabe der Anfrage erfolgt im oberen Teilfenster. Sie kann mehrzeilig sein <strong>und</strong> beliebig<br />
viele Leerzeichen oder Tabulatoren enthalten. Die Schlüsselworte der Sprache können<br />
optional händisch eingegeben, oder durch Anklicken des entsprechenden Symbols in der<br />
linken Toolbar an der aktuellen Cursor-Position in der Eingabemaske eingefügt werden.<br />
56
Senden der Anfrage<br />
Eine Anfrage kann durch Anklicken des Send-Buttons gesendet werden. Da hierzu ein<br />
Datenbank-Login nötig ist, öffnet sich, falls der Nutzer die Login-Daten nicht bereits<br />
eingegeben hat, automatisch ein Fenster zur Entgegennahme des Benutzernamens <strong>und</strong><br />
des Passworts. Im Vorfeld kann die Eingabe der Login-Daten auch über einen Button in<br />
der Toolbar oder über die Menüzeile -> Optionen -> Einstellungen erfolgen. Tritt bei<br />
der Anfrage-Analyse ein Fehler auf, so wird dieser im unteren Ausgabe-Bereich angezeigt,<br />
ansonsten wird die Ergebnistabelle im Ausgabebereich dargestellt.<br />
Eine Anfrage kann durch Anklicken des ToSQL-Buttons auch optional als SQL-Anfrage<br />
im Ausgabe-Bereich ausgegeben werden.<br />
Bedienbarkeit<br />
Zum besseren Verständnis werden für alle Buttons der Oberfläche Tooltips angezeigt,<br />
die kurz die Funktionalität erläutern. Außerdem lässt sich unter dem Menüpunkt Hilfe<br />
eine Hilfedatei anzeigen, die die Benutzeroberfläche <strong>und</strong> die Sprache noch einmal kurz<br />
erläutert.<br />
Weitere Funktionen<br />
Alle Eingabe- <strong>und</strong> Ausgabe-Bereiche lassen sich scrollen sobald der dargestellte oder eingegebene<br />
Inhalt zu groß wird. Zusätzlich können aber einzelne Teilfenster durch kleine<br />
Pfeile in den Trennbalken ein- <strong>und</strong> ausgeblendet werden oder in der Größe durch ziehen<br />
des Trennbalkens manuell verändert werden. Die Anordnung der Fenster lässt sich durch<br />
zwei Buttons in der oberen Toolbar oder ober das Ansicht-Menü verändern. CopyPaste<br />
wird in der Eingabemaske unterstützt, in den Ausgabe-Bereichen ist nur das Kopieren<br />
des Auswahlbereiches möglich. Die Funktionen finden sich im Rechtsklick-Menü, im<br />
Bearbeiten-Menü, oder können über die Standard-Tastenkombinationen benutzt werden.<br />
57
Abbildungsverzeichnis<br />
4.1 Aufbau des Operatorbaumes . . . . . . . . . . . . . . . . . . . . . . . . . . 24<br />
4.2 Beispiel-Baum für die Code-Erzeugnung . . . . . . . . . . . . . . . . . . . 46<br />
5.1 Klassenhierarchie der Knoten-Objekte . . . . . . . . . . . . . . . . . . . . . 51<br />
5.2 Die Klasse TreeBuilder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52<br />
5.3 Klassendiagramm für die Semantische Analyse . . . . . . . . . . . . . . . . 53<br />
5.4 Klassendiagramm für die Code-Erzeugnunge . . . . . . . . . . . . . . . . . 54<br />
6.1 Die Benutzer-Schnittstelle . . . . . . . . . . . . . . . . . . . . . . . . . . . 56<br />
58
Tabellenverzeichnis<br />
4.1 Sprachelemente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15<br />
4.2 Operatoren/Operationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15<br />
4.3 Tokenklassen-Definitionen für Relationenalgebraische Operationen . . . . . 17<br />
4.4 Tokenklassen-Definitionen für arithmetische Operatoren . . . . . . . . . . . 17<br />
4.5 Symboltabelle für die Eingabe PROJ[A Aneu, B+3](R NJOIN S) . . . . . 38<br />
4.6 Übersetzung einzelner Operationen in SQL . . . . . . . . . . . . . . . . . . 39<br />
59