Artikel mit ‘perl’ getagged

Levenshtein mit DBIx::Class

Sonntag, 20. Dezember 2009

Da ich mittlerweile verstärkt mit Catalyst als Framework arbeite und hier als Model-Integration typischerweise DBIx::Class verwendet wird, komme ich nicht umhin, mich weitergehend mit der entsprechenden Syntax auseinanderzusetzen. Dabei mache ich fast jeden Tag die gleiche Erfahrung: DBIx::Class macht die einfachen Anwendungsfälle von SQL noch einfacher, aber die schwierigeren Anwendungsfälle noch viel schwieriger, denn die Klimmzüge, die hier bisweilen in der kruden SQL::Abstract-Syntax zu vollziehen sind, haben es wirklich in sich.

Als Beispiel erstmal der einfache Fall:

my $contact = $schema->resultset('Contact')->search(
  {
    'n_family' => 'Pasche',
    'n_given' => 'Jonas',
    'addresses.city' => 'Mainz'
  },
  { join => 'addresses' }
)->first;

Das ist ja noch durchaus zu überblicken – insbesondere das Zusammenführen mit der Relation ‚addresses‘ ist einfach und elegant.

Will ich nun allerdings die Levenshtein-Distanz mit einbauen, um schreibfehler-tolerant zu suchen, ergeben sich gleich zwei Problemfelder:

  1. Ich brauche eine MySQL-Funktion auf der linken statt auf der rechten Seite des Vergleichsoperators. Okay, prima dokumentiert im DBIx::Class::Manual::Cookbook.
  2. Sowohl auf der linken als auch auf der rechten Seite des Vergleichsoperators sollen Platzhalter (bind values) zum Einsatz kommen: Links der String, mit dem ich vergleichen will; rechts die maximale Distanz, die ich natürlich gerne flexibel als Variable einsetzen möchte. Auch hier hilft noch das Cookbook, wechselt hierbei aber im Beispiel klammheimlich zu literal SQL – gut daran zu erkennen, dass die Suchkriterien nun plötzlich kein Hashref mehr sind. Okay, habe ich nachvollzogen.
  3. Dummerweise möchte ich aber nicht nur einen Wert vergleichen, sondern gleich mehrere. Dabei kann ich das Cookbook-Beispiel natürlich ausbauen und einen entsprechend langen SQL-String selbst schreiben und dann meine ganzen bind values angeben. Das ist jedoch, gelinde gesagt, unglaublich hässlich und vor allem schwer zu warten, weil die bind values innerhalb von Perl an einer ganz anderen Stelle stehen als das SQL-Statement. Möchte ich zum Beispiel ein Feld mittendrin entfernen, muss ich akribisch meine bind values durchzählen, damit ich dort den zugehörigen Wert entferne. So dann bitte lieber nicht.

Nun kann ich auch beim Hashref für die Suchkriterien bleiben, und das funktioniert auch weiterhin, wenn ich SQL-Strings als Hashkeys angebe. Nur wie ich dann auf der rechten Seite, also bei den Hashwerten, die Suchkriterien angeben kann, das hat sich mir einfach nicht erschlossen. Nach längerer Lektüre bleibt aus meiner Sicht nur die Möglichkeit, auch auf der rechten Seite handgeschriebenes SQL zu verwenden, und dort dann die bind values für beide Seiten des Vergleichs anzugeben.

Und so geht’s: Ich gebe den rechten Teil des Vergleichs als doppeltes Arrayref an – die abstrahierte Schreibweise für „Hier kommt jetzt literales SQL mit bind values„. Dabei ist der erste Wert der literale SQL-String; danach folgen die zwei bind values – erst der für links (Vergleichsstring), dann der für rechts (maximale Levenshtein-Distanz) – und da laut Cookbook DBIx::Class hierbei bindtype=columns in SQL::Abstract setzt, muss ich hierbei die etwas krude Syntax der Wertzuweisung über Dummy-Spaltennamen (in meinem Fall einfach „d“) benutzen, wie die oben verlinkte Dokumentation bereits erklärt.

Und so sieht’s final aus:

my $dist = 2;
my $contact = $schema->resultset('Contact')->search(
  {
    'LEVENSHTEIN(n_family, ?)'       => \[ '<= ?', [ d => 'Paschke' ], [ d => $dist ] ],
    'LEVENSHTEIN(n_given, ?)'        => \[ '<= ?', [ d => 'Ionas' ],   [ d => $dist ] ],
    'LEVENSHTEIN(addresses.city, ?)' => \[ '<= ?', [ d => 'Meins' ],   [ d => $dist ] ]
  },
  { join => 'addresses' }
)->first;

Ich bin nicht ganz sicher, ob das wirklich der optimale Weg ist – es erscheint mir ein wenig unsauber, auf der rechten Seite einen bind value mit hineinzumogeln, der eigentlich nach links gehört. Aber ich habe keine sauberere Möglichkeit dafür gefunden – also nehme ich es jetzt mal so hin.

Und so sieht das dann in SQL aus (Ausgabe gekürzt und der besseren Lesbarkeit wegen mit Umbrüchen und Einrückungen versehen; hinter dem Doppelpunkt folgen dann die bind values):

SELECT me.id, [...]
  FROM contact me
       LEFT JOIN address addresses ON addresses.contact_id = me.id
 WHERE (
         (
           LEVENSHTEIN(n_family, ?) <= ?
           AND
           LEVENSHTEIN(n_given, ?) <= ?
           AND
           LEVENSHTEIN(addresses.city, ?) <= ?
         )
       ): 'Paschke', '2', 'Ionas', '2', 'Meins', '2'

Fraglich ist für mich noch, wieso SQL::Abstract das WHERE-Kriterium gleich in doppelte Klammern setzt, wo doch eigentlich nicht einmal einfache vonnöten wären, aber das tut der korrekten Funktion keinen Abbruch.

Hat mich jetzt auch nur zwei Stunden gekostet – für ein SQL-Statement, das ich per Hand aus dem Kopf innerhalb von zwei Minuten fertiggebracht hätte. Wie gesagt: Kompliziertes wird mit SQL::Abstract noch komplizierter. Aber vielleicht ist das ja auch nur dem Umstand geschuldet, dass sich die für SQL::Abstract-Syntax notwendigen Synapsen bei mir noch nicht gebildet haben. Feedback willkommen!

Perl-Modul zur Nutzung der API von InternetX

Freitag, 04. September 2009

Soeben habe ich die erste Version des Perl-Moduls „InternetX“ hochgeladen, mit dem sich die API (Handle-Verwaltung, Domainregistrierung, etc.) über eine „perlige“ Schnittstelle steuern lässt, sowohl als .tar.gz als auch als .src.rpm.

Problematisch war zunächst, dass die InternetX-API bei Domainaufträgen nur eine Rückmeldung im Sinne von „Auftrag angenommen“ liefert und die endgültige Rückmeldung erst im Nachhinein per E-Mail kommt. Wir haben uns daher überlegt, dass es doch ganz praktisch wäre, eine Art Eventliste zu realisieren, um diese periodisch nach zu erledigenden Aufträgen auszuwerten – und dafür eine POP3-Mailbox zu benutzen. Eine Methode eventlist() liefert also den Inhalt einer definierten Mailbox zurück, wobei die Mails hierbei automatisch in ein Hashref geparst werden, das sich leicht weiterverarbeiten lässt. Eine event_delete($uidl)-Methode ermöglicht das „Abhaken“ von Events als erledigt, in dem die betreffende Mail aus der Mailbox entfernt wird – simpel, aber effektiv.

Ich möchte anmerken, dass wir dieses Modul derzeit noch nicht produktiv einsetzen – wenngleich wir das bald umsetzen werden. Sicherlich gibt es hier und da noch etwas zu schleifen, aber letztlich beinhaltet das Modul ohnehin nicht viel eigene Logik, sondern ist in erster Linie Übersetzer zwischen gängigen Perl-Datenstrukturen und den XML-Strukturen, die InternetX verarbeitet. Für Feedback sind wir gerne zu haben; perldoc InternetX liefert außerdem passende Dokumentation mit Beispielen.

IDNA::Punycode und die Abwärtskompatibilität

Dienstag, 28. Juli 2009

Für selfHOST haben wir vor längerer Zeit Teile des Backends entwickelt, darunter den Export von Zonen aus der Datenbank ins DNS-System. Besondere Beachtung erhielten hierbei Umlautdomains: In der Datenbank werden diese „ganz normal“, also mit Umlauten, abgespeichert; beim Export in die DNS-Daten kommt das Perl-Modul IDNA::Punycode zum Einsatz.

Das Modul stellt eine Funktion „encode_punycode“ bereit, die den Punycode-String eines Strings mit UTF8-Werten zurückliefert. Davor musste dann noch der Präfix „xn--„. Eine entsprechende Zeile Programmcode sah nun so also etwa so aus:

$domain = 'xn--' . encode_punycode($domain);

(In Wirklichkeit ist es etwas aufwendiger, weil die einzelnen durch Punkt getrennten Teile eines vollständigen Hostnamens einzeln kodiert werden müssen, aber das sei hier mal dahingestellt.)

Nach Migration eines Teils der Software (die DynDNS-Updates wurden aus Performancegründen vom normalen Webserver separiert) auf einen separaten Server zeigten sich plötzlich merkwürdige Phänomene: Einzelne Domains waren plötzlich im DNS nicht mehr erreichbar, das Gros aber schon. Schnell kamen wir darauf, dass nur Umlautdomains betroffen waren – hier dann aber auch nicht alle. Wie konnte das sein? Die gesamte DynDNS-Update-Routine war ohne jede Änderung 1:1 auf das neue System kopiert worden …

Nach weiteren Analysen stellen wir festen: Wird die DNS-Zone durch den Webserver (altes System) neu geschrieben, zum Beispiel weil jemand DNS-Einträge webbasiert von Hand ändert, wird die Zone korrekt geschrieben. Wird die DNS-Zone durch den DynDNS-Server (neues System) neu geschrieben, tragen alle Umlautdomains einen doppelten Präfix. Von dieser Erkenntnis war es dann nur noch ein kleiner Schritt, um IDNA::Punycode als den Schuldigen auszumachen: Die Funktion setzt nun selbstständig den Präfix davor.

Der Autor bemerkt in der man page:

According to RFC 3490 the ACE prefix „xn--“ had been chosen as the standard.  Thus, „xn--“ is also the default ACE prefix.  For compatibility I’m leaving idn_prefix() in the module.  Use „idn_prefix(undef)“ to get the old behaviour.

Nun, ist ja schön, dass er einem nun diese Arbeit abnimmt. Nur: Schon mal was von Abwärtskompatibilität gehört? Wenn ich eine Funktion gemäß der Dokumentation einsetze, erwarte ich, dass jene auch nach dem x-ten Update bei identischem Aufruf auch noch ein identisches Ergebnis liefert. Wie wär’s zum Beispiel hiermit gewesen:

  • idn_prefix() wird als Funktion eingeführt und ermöglicht einem, mit einer einzigen Zeile Aufwand überall automatisch einen Präfix zu setzen. Benutzt man idn_prefix() nicht, verhält sich das Modul dem bisherigen Default entsprechend. Oder wie wär’s hiermit:
  • encode_punycode() bleibt unangetastet, und es wird eine neue Funktion encode_punycode_with_prefix() eingeführt. Diese berücksichtigt die mittels idn_prefix() vorgenommene Einstellung, die dann meinetwegen auch standardmäßig auf ‚xn--‚ stehen kann.

So aber wurde die Abwärtskompatibilität (ohne Not!) gebrochen und zog auf diese Weise unnötigen Zeitaufwand für Support, Analyse und Fix nach sich. Schade!

Versionshölle: HTML::Mason über RPMforge

Dienstag, 21. April 2009

Wir setzen zur Handhabung des Supports gerne den Request Tracker ein. Der benötigt in der neuesten Version HTML::Mason >= 1.36. Da wir auf den fraglichen Systemen üblicherweise die yum-Repositories von RPMforge eingebunden haben, nutzen wir „yum install perl-HTML-Mason“, was uns aber nur Version 1.32 verschaffte. Da die zu alt ist, haben wir kurzerhand die aktuellste Version 1.40 mittels cpan2rpm von Hand installiert. Die böse Überraschung erfolgte nach einem späteren „yum upgrade“: Aus dem RPMforge-Repository wurde die 1.32 drübergebügelt, und der RT lief nicht mehr.

Mit Blick auf die im Repository enthaltenen Mason-Pakete konnte ich dann feststellen, dass zu einer unseligen Zeit das fragliche RPM mit der Versionsnummer „1.3200“ bereitgestellt wurde. Und, Überraschung: Bei RPMforge gibt es durchaus auch die 1.40 – und auch schon die 1.39, die 1.38, … und eben die 1.3200. Die betrachtet das Versionsmanagement nun aber dummerweise als neueste Version – nicht ganz zu Unrecht, denn mathematisch gesehen ist 3200 nun mal größer als 40.

Also erstmal die 1.40 von Hand installiert und eine Ausnahme in die /etc/yum.conf eingetragen; gleichzeitig Info an Dag Wieers (RPMforge) mit der Bitte um einen Bugfix.

DBI::selectall_arrayref kann auch Hashes

Donnerstag, 11. Dezember 2008

Oftmals stand ich vor dem Problem, eine Reihe von Datensätzen mittels DBI „auf einmal“ abzufragen und das Ergebnis praktikabel vorliegen zu haben. „Praktikabel“ heißt für mich, jeder Datensatz als Hashref. Nun benötige ich aber oftmals keine speziellen Index pro Datensatz, wie ihn selectall_hashref aber verlangt. Manchmal gibt es auch schlicht keinen passablen Index. Dafür möchte ich die Ergebnisse vielleicht sortiert beziehen. Bisher habe ich mir mit einer Krücke beholfen, die aus dem Hashref, dessen Values die gewünschten Hashrefs der einzelnen Datensätze sind, ein (sortiertes) Array aus den Datensatz-Hashrefs produzierte. Hat geklappt. War aber nicht elegant.

Elegant ist jetzt aber, dass selectall_arrayref nicht nur die Ergebnisliste so wie mittels ORDER BY angegeben zurückliefert, sondern auch dazu noch die Datensätze nicht zwingend ebenfalls als Arrayref liefern muss, sondern diese auch als Hashref liefern kann. Und so sieht das aus:

my $arrayref =
  $dbh->selectall_arrayref( $statement, { Slice => {} }, @bind );

Data::Dumper zeigt, wie die Struktur dann ausschaut:

$VAR1 = [
          {
            'key1' => 'value1',
            ...
          },
          {
            'key2' => 'value2',
            ...
          },
          ...
        ];

Neat! :-)

Viel Spaß mit Date::Calc::Delta_YMD()

Mittwoch, 02. April 2008

Für Berechnungen mit Datumsangaben setze ich üblicherweise auf Date::Calc. Es bekommt zwei Array aus je drei Werten (Jahr, Monat, Tag) als Parameter und liefert die Differenz ebenfalls als Array. Nun würde ich damit gerne berechnen, wie alt jemand ist. Da jemand, der 29 Jahre, 4 Monate und 15 Tage alt ist, effektiv nun mal einfach 29 ist, empfehlen entsprechenderweise auch etliche Foreneinträge, für die Berechnung des Alters doch einfach Delta_YMD zu benutzen und nur die Angabe der Jahre zu verwenden. Auf geht’s:

my @age = Delta_YMD( 1978, 12, 17, 2008, 04, 02 );
print join( ", ", @age ) . "\n";

Das Ergebnis überrascht:

30, -8, -15

Für Date::Calc bin ich also bereits über die Bioklippe gesprungen, obwohl es bis dahin noch mehrere Monate sind. Dafür wird ein interessantes Konzept negativer Monate und Tage eingesetzt. Na dankeschön, aus den jeweils drei Werten eines Array einfach dreimal die Differenz zu bilden, hätte ich auch ohne extra Modul hinbekommen.

Eine kurze Recherche im Netz brachte dann diesen Wrapper für Delta_YMD() zutage, mit dem sich das Problem lösen lässt. Vielleicht könnte auch das (nicht offiziell in CPAN enthaltene) Date::Calc::Age helfen. Hab’s aber noch nicht getestet.


Impressum