czwartek, 15 lutego 2007

Ciekawa własność NULLa

W bólach implementacji swoich builderów natknąłem się na ciekawą własność NULLa w MySQL. Nie wiem czy pojawia sie ona również innych SQLach.

Najprościej bedzie na przykladzie. Załózmy, że mamy dwie tabele "process" i "task". Tabela "process" zawiera listę procesów wykonywanych przez pewien abstrakcyjny procesor, na każdy proces składa się pewna ilość zadań, przy czym każde zadanie może należeć tylko do jednego procesu; czyli prosta relacja jeden do wielu. Dodatkowo każde zadanie posiada właściwość "status" określająca stan tego zadania. Dla ustalenia uwagi niech pole "status" może przyjmować trzy wartości: "none", "running" lub "completed". Definicja tych tabel może wyglądac tak:

CREATE TABLE process (
id INT UNSIGNED PRIMARY KEY NOT NULL auto_increment,
name VARCHAR);
CREATE TABLE task (
id INT UNSIGNED PRIMARY KEY NOT NULL auto_increment,
id_process INT UNSIGNED NOT NULL,
status ENUM('none', 'running', 'completed') NOT NULL);

Zapytanie które zwróci nam identyfikator procesu wraz z ilością zadań przyporzadkowanych do tego procesu jest oczywiście trywialne i wyglada tak:

SELECT id_process, COUNT(*) FROM task GROUP BY id_process;

Problemem natomiast jest zapytanie zwracajace identyfikator procesu wraz z ilością zadań przyporzadkowanych do tego procesu, ale takich które nie zostały jeszcze zakończone, czyli mają status różny od 'completed'. Proste:

SELECT id_process, COUNT(status != 'completed') FROM task GROUP BY id_process;
nie wystarcza ponieważ COUNT zlicza zgrupowane wartości bez wzgledu na ich wartość. W tym przypadku zarówno TRUE jak i FALSE jest traktowane jako element to zliczenia. Zatem powyższe zapytanie zwraca to samo co poprzednie, czyli identyfikator procesu wraz z ilością zadań przyporzadkowanych do tego procesu.

Możnaby oczywiście uzyć funkcji SUM i IF w następujący sposób:

SELECT id_process, SUM(IF(status != 'completed), 1, 0) FROM task GROUP BY id_process;
Ale to nie dość, że niezgodne z zasadami sztuki, to jeszcze mało czytelne i pewnie mało wydajne.

Aby w prosty sposób uzyskać oczekiwany wynik wystarczy wykorzystać to że wartości NULLniepoliczalne dla funkcji COUNT. Jeżeli zgrupujemy rekordy które w zadanym polu maja cztery wartości z czego dwie są równe NULL to funkcja COUNT na tym polu zwróci nam 2, a nie 4.

Wystarczy zatem doprowadzić ostatnie zapytanie wykorzystujące funkcję COUNT do takiej postaci, w której wartość argumentu jest policzalna gdy pole "status" jest inne niż 'completed', a niepoliczalna (czyli równa NULL) w przeciwnym wypadku. Wyrażenie

status != 'completed' OR NULL
spełnia te założenia ponieważ przyjmuje jedną z dwóch wartości: TRUE dla pola "status" różnego od 'completed', albo NULL w przeciwnym wypadku.

Ostatecznie zapytanie zwracajace identyfikator procesu wraz z ilością zadań przyporzadkowanych do tego procesu, takich które nie zostały jeszcze zakończone wygląda tak:

SELECT id_process, COUNT(status != 'completed' OR NULL) FROM task GROUP BY id_process;

Śliczne, prawda?

wtorek, 6 lutego 2007

Dlaczego CVS ssie...

W gruncie rzeczy tekst ma być o tym dlaczego SVN jest lepszym wyborem w przypadku repozytorium pakietów. Swoje przemyślenia opieram na bez mała trzy i pół rocznym doświadczeniu z repozytorium http://svn.pld-freebsd.org/svn/packages, dostepnym równiez przez ViewCVS: http://svn.pld-freebsd.org/cgi-bin/viewsvn/.

Struktura repozytorium różni sie znacząco od płaskiego modelu repozytorium stosowanego w PLD. W dużym uproszczeniu wygląda ona tak:

packages
+- trunk
| +- db
| | +- SOURCES
| | | +- patch1.patch
| | | +- patch2.patch
| | +- SPECS
| | +- db.spec
| +- rpm
| +- SOURCES
| | +- patch1.patch
| | +- patch2.patch
| +- SPECS
| +- rpm.spec
+- branches
| +- rpm-4.1.1
| | +- db
| | | +- SOURCES
| | | | +- patch1.patch
| | | | +- patch2.patch
| | | +- SPECS
| | | +- db.spec
| | +- rpm
| | +- SOURCES
| | | +- patch1.patch
| | | +- patch2.patch
| | +- SPECS
| | +- rpm.spec
| +- rpm-4.4.1
| +- db
| | +- SOURCES
| | | +- patch1.patch
| | | +- patch2.patch
| | +- SPECS
| | +- rpm.spec
| +- rpm
| +- SOURCES
| | +- patch1.patch
| | +- patch2.patch
| +- SPECS
| +- rpm.spec
+- tags
+- Ac-rpm-4.4.1-2
+- db
| +- SOURCES
| | +- patch1.patch
| | +- patch2.patch
| +- SPECS
| +- rpm.spec
+- rpm
+- SOURCES
| +- patch1.patch
| +- patch2.patch
+- SPECS
+- rpm.spec

Jak widać na powyższym schemacie dla każdego pakietu została wydzielona osobna gałąź w drzewie repozytorium. Taki zabieg, poza uporzadkowaniem i separacją składników poszczególnych pakietów, pozwala na rozdzielenie przestrzeni nazw plików w pakietach. A co za tym idzie, powoduje że np. nazwa patcha może być dowolna dla zadanego pakietu i nie istnieje konieczność pilnowania żeby nazwy łatek dla różnych pakietów nie kolidowały ze sobą. W szczególności, nie ma potrzeby narzucania nazewnictwa tych łatek do postaci nazwa_pakietu-nazwa_łatki.patch jak ma to miejsce w PLD.

Ściagnięcie całego drzewa trunk może wygląda nastepująco:

svn checkout http://svn.pld-freebsd.org/svn/packages/trunk packages
Natomiast ściagniecie i kompilacja pakietu db z takiego repozytorium wygląda tak:
$ cd ~/packages
$ svn checkout http://svn.pld-freebsd.org/svn/packages/trunk/db
$ cd db/SPECS
$ rpmbuild -ba --define "_topdir ~/packages/db" db.spec
Powyższy przykład nie uwzględnia oczywiście ściagnięcia ewentualnych tarballi z distfiles.

Drzewiastej strukturze repozytorium można zarzucać niemozliwość "przegrepowania" wszystkich specy, albo wprowadzania masowych zmian w specach. Wadę tą można wyeliminować używając polecenia find. I tak odpowiednikiem grep jakisstring * w katalogu SPECS jest

find . -name "*.spec" -exec grep -H jakisstring {} \;
w katalogu packages/trunk.

Praca z takim repozytorium nie jest dużo bardziej skomplikowana, niż z płaskim repozytorium CVSowym. A prawie zupełnie nie różni się gdy użyjemy odpowiednio zmodyfikowanego skryptu builder.

Kolejną przewagą nad CVSem są "atomowe" commity. Polega to na tym, że po wprowadzeniu zmian z pakiecie, zmiany są przekazywane do repozytorium jednym poleceniem, np.

$ cd ~/packages
$ svn commit -m "- updated to 4.5\n- updated patch1 patch" db
Co najważniejsze, takie dokonanie zmiany jest traktowane w repozytorium jako integralna całość identyfikowana numerem rewizji w repozytorium, a nie jest złożeniem zmian w poszczególnych plikach. Co za tym idzie możemy łatwo odszukać, że np. update db do wersji 4.5 pociągneło za sobą usuniecie pliku SOURCES/patch1.patch. W CVSie, niestety, każda tego typu zmiana jest w repozytorium traktowana osobno, dlatego dużo wiecej pracy kosztuje odnalezienie jak zmieniły się pozostałe pliki pakietu, podczas określonej zmiany pliku spec. Takie zachowanie CVSa jest dla mnie szczególnie uciążliwe, ponieważ w swojej pracy intensywnie śledzę zmiany w repozytorium PLD w celu nanoszenia niektórych z nich w PLD/FreeBSD.

Atomowość commitów w repozytorium pociaga za sobą również zwiekszenie przejrzystości commitlogów, dzięki temu są one generowane jako pojedynczy mail, a nie dwa osobne maile zawierajace zmiany odpowiednio, w module SPECS i w module SOURCES.

Kolejna przewagą Subversion jest możliwość przenoszenia plików (z zachowaniem historii zmian) z poziomu użytkownika. W CVSie taka operacja wymaga zaangażowania administratora repozytorium, który musi dokonać takiej zmiany po stronie serwera.

Dla niektórych kolejną wadą SVNa może być niemożliwość generowania sekcji %changelog, tak jak ma to miejsce w chwili obecnej w PLD. Według mnie używanie $Log$ do generowania loga to średnio dobry pomysł, tym bardziej, że taki log nie jest tworzony w formacie przyjmowanym przez RPMa. Słuszniej byłoby gdyby %changelog był generowany z loga SVNa na source-builderach w formacie natywnym dla RPMa.

Na koniec wypada jeszcze wspomnieć o łatwości instalacji serwera Subversion. Po raz pierwszy zajeło mi to kilkanaście minut, podczas gdy na instalację serwera CVSu straciłem kilka(naście) godzin.