My Shiny Weblog!

programming, photography and lifestyle

Разширяване на TinyScheme със SQLite

TinyScheme е един много малък Scheme интерпретатор, който през годините е използван за най-различни интересени проеки. Част от тях законни, други не особено. Той е написан на C и с помощта на макроси към препроцесора може да бъде компилиран с различна функционалност – според нуждите на потребителите. Естествено в самия интерпретатор е реализиран абсолютния минимум на езика. Очаква се потребителите да дописват каквото им потрябва. На мен ми потрябва база данни. Логичния избор за такава беше SQLite. Поради минималните си размери и простата организация – двете си пасват като дупе и гащи. Една хубава библиотека, която разширява TinyScheme възможностите е TSX. Тя дава възможност за работа със сокети и файлове. Написана е много хубаво и всичко вътре е изключително просто за разбиране. Вместо да правя отделна библиотека реших да допиша тази. За да можем да работим със SQLite са необходими поне следните функции – sqlite-open, sqlite-close, sqlite-prepare, sqlite-bind-text, sqlite-step, sqlite-column-text. Съответно трябва да можем да отваряме нова база и да я затваряме. Да правим sqlite3_stmt, да закачаме стойности към него, да го изпълняваме на отделни стъпки и да можем да вземаме стойностите от SELECT заявките. Естествено има още много полезни SQLite функции, които може да потрябват, но тези са един хубав минимум, от който можем да започнем. Разглеждайки TSX сорс кода, лесно можем да се ориентираме как се пишат TinyScheme функции. Всички функции връщат тип pointer, като параметри им се предават указател към интерпретатора и указател към аргументите на функцията. Цялата TinyScheme функционалност можем да ползваме през първия указател. Ето как изглежда sqlite-open.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pointer foreign_sqliteopen(scheme * sc, pointer args) {
	sqlite3 *sqlite;
	pointer first_arg;
	int retcode;

	if(args == sc->NIL)
		return sc->F;

	first_arg = sc->vptr->pair_car(args);
	if(!sc->vptr->is_string(first_arg))
		return sc->F;

	retcode = sqlite3_open(sc->vptr->string_value(first_arg), &sqlite);
	if (retcode == -1)
		return sc->F;

	return sc->vptr->mk_integer(sc, (ptr)sqlite);
}

Най-напред проверяваме дали са ни подадени някакви аргументи изобщо, ако не са връщаме #f. След това вземаме първия аргумент. Понеже аргументите ни се подават, като списък (естествено) първия аргумент на функцията е car на списъка. Проверяваме дали той е низ, ако не е връщаме #f. Понеже повече аргументи не ни трябват – викаме sqlite3_open, като подаваме низ от първия аргумент. По този начин инициализираме sqlite3 указател, който след това връщаме. Понеже нямаме специален тип за sqlite3 в TinyScheme, използваме mk_integer(). Тоест връщаме стойността на указателя, като int или long стойност, в зависимост от архитектурата. Това най-вероятно не е най-добрия начин за вършене на тази работа, но със сигурност е най-простия. Ако искаме да постигнем дуракоустойчива система, в която да не разнасяме C указатели из Scheme интерпретатора, можем да му допишем базовите типове. Как става това ще опиша след малко. Вече можем да отворим нова база данни, но е добре да можем и да я затворим – sqlite-close.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pointer foreign_sqliteclose(scheme * sc, pointer args) {
	pointer first_arg;
	if(args == sc->NIL)
		return sc->F;

	first_arg = sc->vptr->pair_car(args);
	if(!sc->vptr->is_number(first_arg))
		return sc->F;

	sqlite3 *sqlite = sc->vptr->ivalue(first_arg);
	sqlite3_stmt *pStmt;
	while((pStmt = sqlite3_next_stmt(sqlite, 0)) != 0 ) {
   	     sqlite3_finalize(pStmt);
	}

	sqlite3_close(sqlite);
	return sc->T;
}

Тук си вземаме обратно C указателя, за целта проверяваме дали единствения аргумент е integer. Преди да затворим базата, затваряме и освобождаваме всички sqlite3_stmt отворени към нея. Това ни спестява писане на отделна функция за тази работа. Следваща функция, която ни трябва е sqlite-prepare, тя ще създава sqlite3_stmt обектите (prepared statements).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
pointer foreign_sqliteprepare(scheme * sc, pointer args) {
	pointer first_arg;
	pointer second_arg;
	int retcode;

	if(args == sc->NIL)
		return sc->F;

	first_arg = sc->vptr->pair_car(args);
	if(!sc->vptr->is_number(first_arg))
		return sc->F;

	args = sc->vptr->pair_cdr(args);
	second_arg = sc->vptr->pair_car(args);
	if(!sc->vptr->is_string(second_arg))
		return sc->F;

	sqlite3_stmt *stmt;
	retcode = sqlite3_prepare((sqlite3*)sc->vptr->ivalue(first_arg), sc->vptr->string_value(second_arg),
	     -1, &stmt, (const char **)NULL);

	if(retcode != SQLITE_OK)
		return sc->F;

	return sc->vptr->mk_integer(sc, (ptr)stmt);
}

Тук вече имаме два аргумента на функцията – указател към базата данни и низ – SQL заявка. За да вземем втория правим cdr на списъка, и после отново car. Викаме sqlite3_prepare и ако всичко е наред връщаме указател към sqlite3_stmt. Трябва да можем да вържем стойностите към заявката със sqlite-bind-text.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
pointer foreign_sqlitebindtext(scheme * sc, pointer args) {
	pointer first_arg, second_arg, third_arg;
	int retcode;

	if(args == sc->NIL)
		return sc->F;

	first_arg = sc->vptr->pair_car(args);
	if(!sc->vptr->is_number(first_arg))
		return sc->F;

	args = sc->vptr->pair_cdr(args);
	second_arg = sc->vptr->pair_car(args);
	if(!sc->vptr->is_number(second_arg))
		return sc->F;

	args = sc->vptr->pair_cdr(args);
	third_arg = sc->vptr->pair_car(args);
	if(!sc->vptr->is_string(third_arg))
		return sc->F;

	retcode = sqlite3_bind_text(
			(sqlite3_stmt*)sc->vptr->ivalue(first_arg),
			sc->vptr->ivalue(second_arg),
			sc->vptr->string_value(third_arg),
			-1, SQLITE_STATIC);

	if(retcode != SQLITE_OK) {
		return sc->F;
	}

	return sc->T;
}

Аргументите са три, вземаме ги с необходимото количество car и cdr. Това е малко досадно, но с достатъчно мотаене на разни списъци, човек става силен car, cdr caar, cadr, … нинджа. Лепим текста за заявката и ако всичко е наред – връщаме #t. Вече можем да я изпълним със sqlite-step.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pointer foreign_sqlitestep(scheme * sc, pointer args) {
   pointer first_arg;
   int retcode;

   if(args == sc->NIL)
      return sc->F;

   first_arg = sc->vptr->pair_car(args);
   if(!sc->vptr->is_number(first_arg))
      return sc->F;

   retcode = sqlite3_step((sqlite3_stmt*)sc->vptr->ivalue(first_arg));
   if(retcode != SQLITE_DONE && retcode != SQLITE_ROW)
      return sc->F;

   return sc->T;
}

Вземаме указател към базата, изпълняваме стъпката и ако всичко е наред – връщаме #t. Остава да напишем sqlite-column-text.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pointer foreign_sqlitecolumntext(scheme * sc, pointer args) {
	pointer first_arg;
	pointer second_arg;
	char *ret;

	if(args == sc->NIL)
		return sc->F;

	first_arg = sc->vptr->pair_car(args);
	if(!sc->vptr->is_number(first_arg))
		return sc->F;

	args = sc->vptr->pair_cdr(args);
	second_arg = sc->vptr->pair_car(args);
	if(!sc->vptr->is_number(second_arg))
		return sc->F;

	ret = sqlite3_column_text((sqlite3_stmt*)sc->vptr->ivalue(first_arg), sc->vptr->ivalue(second_arg));

	if (ret == NULL)
		return sc->F;

	return sc->vptr->mk_string(sc, ret);
}

Тук подаваме sqlite3_stmt указател и номер на полето, което искаме да вземем от съответната заявка. Ако всичко е наред връщаме стойността на полето, като низ. Следващата стъпка е да опишем имената на всички функции, така че интерпретатора да може да ги намери, когато заредим библиотеката. С други думи – трябва да регистрираме имената на новите символи. Това става във функцията init_tsx() или съответна за модула init функция (init_sqlite(), ако пишем отделен модул).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifdef HAVE_SQLITE
  sc->vptr->scheme_define(sc, sc->global_env,
		sc->vptr->mk_symbol(sc, "sqlite-open"),
		sc->vptr->mk_foreign_func(sc, foreign_sqliteopen));
 sc->vptr->scheme_define(sc, sc->global_env,
		sc->vptr->mk_symbol(sc, "sqlite-close"),
		sc->vptr->mk_foreign_func(sc, foreign_sqliteclose));
  sc->vptr->scheme_define(sc, sc->global_env,
		sc->vptr->mk_symbol(sc, "sqlite-prepare"),
		sc->vptr->mk_foreign_func(sc, foreign_sqliteprepare));
  sc->vptr->scheme_define(sc, sc->global_env,
		sc->vptr->mk_symbol(sc, "sqlite-bind-text"),
		sc->vptr->mk_foreign_func(sc, foreign_sqlitebindtext));
  sc->vptr->scheme_define(sc, sc->global_env,
		sc->vptr->mk_symbol(sc, "sqlite-step"),
		sc->vptr->mk_foreign_func(sc, foreign_sqlitestep));
 sc->vptr->scheme_define(sc, sc->global_env,
		sc->vptr->mk_symbol(sc, "sqlite-column-text"),
		sc->vptr->mk_foreign_func(sc, foreign_sqlitecolumntext));
#endif /* defined (HAVE_SQLITE) */

Накрая можем да компилираме TSX и да напишем една елементарна тестова програма sqlite.scm:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
(load-extension "tsx-1.1/tsx")
(define (db-create sqlite)
   (define query "CREATE TABLE test (a varchar, b varchar)")
   (define stmt (sqlite-prepare sqlite query))
   (sqlite-step stmt))

(define (db-insert sqlite a b)
  (define stmt (sqlite-prepare sqlite "INSERT INTO test VALUES (?, ?)"))
  (sqlite-bind-text stmt 1 a)
  (sqlite-bind-text stmt 2 b)
  (sqlite-step stmt))

(define (db-select sqlite)
   (define stmt (sqlite-prepare sqlite "SELECT * FROM test"))
   (sqlite-step stmt)
   (define a (sqlite-column-text stmt 0))
   (define b (sqlite-column-text stmt 1))
   (display (string-append a b))
   (newline))

(delete-file "test.db")
(define sqlite (sqlite-open "test.db"))
(db-create sqlite)
(db-insert sqlite "it is" " working!")
(db-select sqlite)
(sqlite-close sqlite)

Този пример тества всички написани до момента функции. Остана да напиша как се правят нови типове данни в интерпретатора. Описание за това има в документацията към TinyScheme, то е разбираемо, но не е много актуално. Дописването на нови типове си заслужава, ако се налага използване на динамична памет. Във всички написани до тук функции никъде не използвахме явно динамична памет. Такава памет се използва от самия SQLite. По тази причина ако използваме често sqlite-prepare ще направим memory leak, защото използваната памет се освобождава чак когато затворим базата. Този проблем може да се реши, ако напишем функция sqlite-finalize. sqlite-close и sqlite-finalize обаче не са в “духа” на езика Scheme. Много по-добре би било, ако самия интерпретатор се грижи за освобождаването на паметта, с помощта на garbage collector. TinyScheme използва Schorr-Deutsch-Waite link-inversion algorithm, от The Art of Computer Programming, том 1. Тип string от езика може да се използва, като пример за създаване на нови типове данни, които да използват динамична памет. На мен ми трябваше тип BLOB, в който да пазя разни парчета неформатирани двойчни данни. Този тип разбира се, може да се ползва и за записване на такива данни в SQLite BLOB полета. За целта можем да допишем функциите sqlite-bind-blob и sqlite-column-blob.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
pointer foreign_sqlitebindblob(scheme * sc, pointer args) {
	pointer first_arg, second_arg, third_arg;
	int retcode;

	if(args == sc->NIL)
		return sc->F;

	first_arg = sc->vptr->pair_car(args);
	if(!sc->vptr->is_number(first_arg))
		return sc->F;

	args = sc->vptr->pair_cdr(args);
	second_arg = sc->vptr->pair_car(args);
	if(!sc->vptr->is_number(second_arg))
		return sc->F;

	args = sc->vptr->pair_cdr(args);
	third_arg = sc->vptr->pair_car(args);
	if(!sc->vptr->is_blob(third_arg))
		return sc->F;

	retcode = sqlite3_bind_blob(
			(sqlite3_stmt*)sc->vptr->ivalue(first_arg),
			sc->vptr->ivalue(second_arg),
			sc->vptr->blob_value(third_arg),
			sc->vptr->blob_size(third_arg), SQLITE_STATIC);

	if(retcode != SQLITE_OK) {
		return sc->F;
	}

	return sc->T;
}

За да работи тази функция, в интерпретатора трябва да имаме дефинирани функциите is_blob(), blob_value() и blob_size(). Човек лесно може да си ги напише, като гледа от съответните string функции. Няма да ги пиша тук, защото ще стане много дълго.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
pointer foreign_sqlitecolumnblob(scheme * sc, pointer args) {
	pointer first_arg;
	pointer second_arg;
	char *ret;
	int len;

	if(args == sc->NIL)
		return sc->F;

	first_arg = sc->vptr->pair_car(args);
	if(!sc->vptr->is_number(first_arg))
		return sc->F;

	args = sc->vptr->pair_cdr(args);
	second_arg = sc->vptr->pair_car(args);
	if(!sc->vptr->is_number(second_arg))
		return sc->F;

	ret = sqlite3_column_blob((sqlite3_stmt*)sc->vptr->ivalue(first_arg), sc->vptr->ivalue(second_arg));

	len = sqlite3_column_bytes((sqlite3_stmt*)sc->vptr->ivalue(first_arg), sc->vptr->ivalue(second_arg));

	if (ret == NULL)
		return sc->F;

	return sc->vptr->mk_blob(sc, ret, len);
}

Ето тук е интересната част. Използваме mk_blob() за да регистрираме новия тип в интерпретатора. Освобождаването на паметта е грижа на garbage collectior-а. Ако искаме да бъдем перфекционисти, освен BLOB можем да напишем sqlite3 и sqlite3_stmt, като типове в езика. Аз обаче не съм си играл да го правя. Дори в този си вид TinyScheme и SQLite представляват доста мощно средство за писане най-различни програми. В момента работя по една data mining система. Тъй като SQLite се използва масово в Mac OS X, iPhone OS и Series 60, написана на TinyScheme тя ще може да работи лесно на всички тези платформи.