random text record identifiers

polish disclaimer begin;

w celu trenowania języka, oraz by poszerzyć teoretyczny zasięg bloga będę teraz starał się pisać po anglijsku. wytykanie błędów mile widziane.

polish disclaimer commit;

ok, so you're trying to build something that needs random-text record identifiers. perhaps a new tinyurl-kind-of-service.

and you are thinking about a way to implement random text generation in a way that:

  • there will be no direct information which record was added just after given one (knowing it's textual id). i mean – the text id's cannot be sequential like a, b, c, d, …
  • the code should be as small as possible. we do not want to start with 40-characters behemoths just to make sure no-one can know which ones were before another.
  • it should be as simple as possible.

requirements 1 and 2 are almost contrary, but we can manage.

first – let's assume we will generate random id's out of these characters: a-z, A-Z, 0-9. this gives us 62 different characters.

now. let's do it that way:

  1. assume current_length to be 1
  2. generate random string of length = current_length
  3. if this id is already taken, increment current_length and repear from step 2
  4. voila. new id generated.

it matches both first and second requirement from list.

as for simplicity.

let's try to implement:

first, let's create a test table with unique constraint on text-id field (i keep numerical id “just in case"):

CREATE TABLE test_table (
id          BIGSERIAL,
random_code TEXT  NOT NULL DEFAULT '',
something   TEXT ,
PRIMARY KEY (id)
);
CREATE UNIQUE INDEX ui_test_table_random_code ON test_table (random_code);

now, let's create function for random string generation:

CREATE OR REPLACE FUNCTION get_random_string(string_length INT4)
RETURNS TEXT
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
possible_chars TEXT = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
output TEXT = '';
i INT4;
pos INT4;
BEGIN
FOR i IN 1..string_length LOOP
pos := 1 + CAST( random() * ( LENGTH(possible_chars) - 1) AS INT4 );
output := output || substr(possible_chars, pos, 1);
END LOOP;
RETURN output;
END;
$BODY$;

code looks simply i guess. in case it doesn't – ask question in comments area.

now. the master-code, which is a trigger:

CREATE OR REPLACE FUNCTION trg_test_table_get_random_code()
RETURNS TRIGGER
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
string_length INT4 = 1;
temprec RECORD;
new_string TEXT;
BEGIN
LOOP
new_string := get_random_string(string_length);
SELECT COUNT(*) INTO temprec FROM test_table WHERE random_code = new_string;
IF temprec.count = 0 THEN
NEW.random_code := new_string;
EXIT;
END IF;
string_length := string_length + 1;
IF string_length >= 30 THEN
raise exception 'random string of length == 30 requested. something''s wrong.';
END IF;
END LOOP;
RETURN NEW;
END;
$BODY$;
CREATE TRIGGER trg_test_table_get_random_code BEFORE INSERT ON test_table FOR EACH ROW EXECUTE PROCEDURE trg_test_table_get_random_code();

basically this trigger is implementation of algorithm i wrote couple of lines above.

is it done? basically yes.

after 10 inserts i got this content of table:

# SELECT * FROM test_table;
id | random_code | something
----+-------------+-----------
1 | x           | x
2 | B           | x
3 | w           | x
4 | U           | x
5 | b           | x
6 | OE          | x
7 | N           | x
8 | zn          | x
9 | Y           | x
10 | JY          | x
(10 ROWS)

if you know your database-things you will see that the code has one serious (or not serious, depending on your view) problem.

if two inserts will happen at the same time, it is possible that one of them will raise exception of unique violation.

this is because there is a race condition between select count(*) and actual insert which takes place only after trigger finishes.

is there no hope? there is. but we have to modify the way we insert data to test table.

until now, i was able to simply: insert into test_table (something) values (‘x'); and it called my trigger code which set the random_code to whatever ‘s appropriate.

but if i want this to be a more fault-proof, i need to throw away the trigger, and force client to do inserts using select's.

like this:

CREATE OR REPLACE FUNCTION smart_insert(IN in_something TEXT, OUT new_key TEXT)
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
string_length INT4 = 1;
BEGIN
LOOP
new_key := get_random_string(string_length);
BEGIN
INSERT INTO test_table(something, random_code) VALUES (in_something, new_key);
RETURN;
EXCEPTION WHEN unique_violation THEN
-- do nothing
END;
string_length := string_length + 1;
IF string_length >= 30 THEN
raise exception 'random string of length == 30 requested. something''s wrong.';
END IF;
END LOOP;
END;
$BODY$;

and now, i can:

SELECT smart_insert('x');

done.

or is it?

what if you'd like to be able to generate these text keys in more than one table? (for simplicity sake let's assume all of them have the same fields).

in such a case we would modify the function to be:

CREATE OR REPLACE FUNCTION smart_insert(IN TABLE_NAME TEXT, IN in_something TEXT, OUT new_key TEXT)
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
string_length INT4 = 1;
use_sql TEXT;
BEGIN
LOOP
new_key := get_random_string(string_length);
BEGIN
use_sql := 'INSERT INTO ' || quote_ident( TABLE_NAME ) || ' (something, random_code) VALUES (' || quote_literal(in_something) || ', ' || quote_literal(new_key) || ')';
EXECUTE use_sql;
RETURN;
EXCEPTION WHEN unique_violation THEN
-- do nothing
END;
string_length := string_length + 1;
IF string_length >= 30 THEN
raise exception 'random string of length == 30 requested. something''s wrong.';
END IF;
END LOOP;
END;
$BODY$;

and then i could do:

SELECT smart_insert('test_table', 'xxx');

print “read manual” while each %bug;

taak.

trafił mnie dziś bardzo “fajny" błąd.

w sofcie który pisałem, piszę i się zajmuję mam taki kawałek kodu:

for my $object ( @objects ) {
next unless $self->validate_object( $object );
$self->save_object_to_database( $object );
}

kod ten pobiera z podanej listy obiekty (tak naprawdę to nie obiekty tylko struktury (hasze haszy). potem waliduje zawartość i jeśli walidacja się udała – wpisuje do bazy.

trywiał.

jednym z elementów walidacji jest podmiana pewnych wartości na wartości słownikowe.

powiedzmy, że mamy w “obiekcie" element “region" i ma on wartość “WARSZAWA". w odpowiednim słowniku sprawdzam czy wartość WARSZAWA jest dopuszczalna. jak nie – walidacja się nie udała. jak tak, zamiast stringu “WARSZAWA" wstawiam numeryczny identyfikator z bazy. np. 15.

kod który to robi:

sub validate_object {
my $self = shift;
my $object = shift;
while (my ($param, $dictionary) = each %{ $self->validation_rules }) {
next unless defined $object->{ $param };
my $object_value = $object->{ $param };
if ( $self->dictionaries->{ $dictionary }->{ $object_value } ) {
$object->{ $param } = $self->dictionaries->{ $dictionary }->{ $object_value };
next;
}
$self->log("error at validation ...");
return;
}
return 1;
}

może nie jest to najbardziej czytelne, ale po kolei:

hash $self->validation_rules ma pary klucz/wartość, gdzie klucz jest nazwą klucza (elementu) z obiektu, a wartość jest nazwą słownika którym mamy dany element walidować.

przykładowo hash ten może zawierać:

"region" => 'REGION_LIST'

reguł jest standardowo około 10.

słowniki są zwracane z metody $self->dictionaries(). zwracana struktura to hashref, mający jako klucz nazwę słownika, a jako wartość – hashref z parami – tekstowa wartość => numeryczny identyfikator.

przykładowo:

{
'REGION_LIST' => { 'WARSZAWA' => 1, 'KRAKÓW' => 2, ...},
'CATEGORIES_LIST' => { 'MOTO' => 15, 'AGD' => 21, ... },
...
}

proste.

czy widzicie błąd w kodzie funkcji validate_object() ?

nie?

podpowiem. objaw który do mnie trafił, to to, że metoda save_object_to_database() zwracała bład sql'a, który mówił, że wartość ‘WARSZAWA' nie jest prawidłowa dla pola numerycznego.

nadal nie wiecie?

otóż metoda each(). zapamiętuje ona w haszu ostatnio zwrócony element. tak aby przy następnym wywołaniu zwrócić kolejny.

co się więc stanie gdy któryś z obiektów sie nie zwaliduje?

załóżmy, że mamy 10 reguł. od 1 do 10. przy obiekcie “a" zwalidowały się reguły 1, 2, 3, 4, a przy regule 5 pojawił się błąd. został zalogowany ($self->log), metoda validate_object się skończyła pustym returnem. więc w głównej metodzie został pobrany kolejny obiekt – “b".

przy walidowaniu obiektu “b", wywołujemy each(), który zwraca którą regułę? 6! potem 7, 8, 9, 10 i na tym skończy. czyli reguły 1-5 w ogóle nie są sprawdzone. i nawet jeśli obiekt jest poprawny – wartości odpowiednich pól nie zostają zamienione na id'y. i stąd błąd przy insercie.

czemu o tym piszę?

dwa powody.

po pierwsze: może się to komuś przyda.

po drugie: może to spowoduje, że o tym nie zapomnę. i następnym razem zamiast bawić się each() użyję po prostu keys.

tsearch – instalacja, testy, rozszerzanie

jak część z was wie byłem ostatnio na pgcon'ie.

tu od razu informacyjnie – byłem dzięki mojemu pracodawcy – firmie eo networks, któremu niniejszym publicznie bardzo dziękuję za umożliwienie mi wzięcia udziału w tej imprezie – jeśli szukacie pracy i znacie się na javie, bazach danych (głównie postgres, ale slyszałem też o jakichś projektach na innych bazach) – warto się odezwać.

wracając do meritum.

byłem tam na prezentacji olega bartunova nt. nowego tsearcha. nowego – nie znaczy, że będzie tsearch3. nowego – czyli tsearch2 zintegrowanego z samym postgresem.

jak się uda wszystko co zaplanowali to będzie tak w 8.3, ale jak się nie uda – no cóż. zobaczymy.

na prezentacji podpatrzyłem jedną rzecz którą wam tu teraz pokażę: wyszukiwanie pełnotekstowe z “poprawianiem" literówek (fuzzy-full text search).

Continue reading tsearch – instalacja, testy, rozszerzanie

parsowanie rss’ów

prace nad dnews postępują powoli. bardzo powoli.

pisałem dziś kawałek kodu do czytania rss'ów/atom'ów/rdf'ów.

prosty test na rss'ach planety ubuntu i wynik:

undefined entity at line 527, column 19, byte 70093 at /usr/lib/perl5/XML/Parser.pm line 187

wrrrrrr.

a co jest w tej linii?

=> head -n 527 planet.ubuntulinux.org.xml | tail -n 1
<title>Sebastian K&uuml;gler: We are out of real life</title>

jeśli nie zgadłeś – problemem jest ten &uuml;.

co z tym zrobić? popytałem na ircu, poszukałem na googlu. w końcu trafiłem na stronę która mówi o przyczynach i jak zapobiec.

w związku z tym musiałem zastosować taki kawałek hacka:

$content =~ s{
\A
(
\s*
<\?.*\?>
\s*
)
<
([A-Z0-9:_-]+)
}{$1 . get_doctype($2) . "<" . $2}eixms;

gdzie funkcja get_doctype wygląda:

sub get_doctype {
my $tag = shift;
return <<__DOCTYPE__;
<!DOCTYPE $tag [
<!ENTITY % HTMLlat1 PUBLIC "-//W3C//ENTITIES Latin 1 for XHTML//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml-lat1.ent"> %HTMLlat1;
<!ENTITY % HTMLspecial PUBLIC "-//W3C//ENTITIES Special for XHTML//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml-special.ent"> %HTMLspecial;
<!ENTITY % HTMLsymbol PUBLIC "-//W3C//ENTITIES Symbols for XHTML//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml-symbol.ent"> %HTMLsymbol;
]>
__DOCTYPE__
}

co to powoduje?

wstawia przed pierwszy tag w xml'u deklarację używania entity z html'a.

po tym – xml::feed działa mi już poprawnie nie marudząc o nic 🙂

wniosek? część (zgaduję, że większość) feedów rdf/rss/atom jest zwalona jeśli chodzi o poprawność xml'a.