sqlite3 에서 virtual table 을 구현한 것 중 기본 제공하는 기능이 csv 파일을 table 처럼 읽는 기능을 제공하는 csvtab이다. virtual table 을 분석하기 아주 좋은 구조라서 정리해 보았다. 해당 코드는 https://github.com/sqlite/sqlite/blob/version-3.38.5/ext/misc/csv.c  에서 볼 수 있다. 

 우선 csvtab 은 write 하는 기능이 없고, 당연히 transaction 관련 기능도 없다. 그래서 간단한 편이다. 앞에서 설명한(https://yiunsr.tistory.com/879) 함수이름에 x 접두어 대신 csvtab 접두어를 사용하고 있다. 

csvtab 을 이용하는 방법은 

CREATE VIRTUAL TABLE temp.csv USING csv(filename=FILENAME);


 이렇게 하거나 

CREATE VIRTUAL TABLE temp.csv2 USING csv(
    filename = "../http.log",
    schema = "CREATE TABLE x(date,ipaddr,url,referrer,userAgent)"
);

이렇게 schema 를 전달 할 수 있다. 또 data 인자롤 통해서 실제 csv 데이터를 넣을 수도 있다. 실제 예제를 찾지는 못했지만 아래처럼 될 것 같다.

CREATE VIRTUAL TABLE temp.csv2 USING csv(
    data = "2021-01-01,1.120.12.12,http://google.com,,chrome\n",
    schema = "CREATE TABLE x(date,ipaddr,url,referrer,userAgent)"
);



CsvReader (https://github.com/sqlite/sqlite/blob/version-3.38.5/ext/misc/csv.c#L74)
* csv 파일에 대한 handler 를 가지고 있고, 해당 파일을 읽은 데이터를 buffer 에 저장해주는 기능을 함
* csv를 파싱할 때 필요한 데이터들을 저장하고 있음. 
* nLine 은 현재 line 을 의미하는데 에러 표시를 위해 사용된다. (컴파일러 이런 것에 몇째 줄에서 오류가 있음. 이런 것 표시할 때 사용하는 line) line 은 csv 와 row 와 다르다. csv 는 column 에 " 를 이용해서 column 안에 Newline(https://ko.wikipedia.org/wiki/%EC%83%88%EC%A4%84_%EB%AC%B8%EC%9E%90) 을 가질 수 있는데 이럴 경우 csv row 는 증가하지 않지만 nLine 은 증가한다. 

CsvTable(https://github.com/sqlite/sqlite/blob/version-3.38.5/ext/misc/csv.c#L307)
* sqlite3_vtab 가 첫 멤버인데, C++의 상속의 역할을 하는 것 같음. using csv 를 사용할 때, 전달되는 paramer 인 filename, data 를 저장하고 csv 파일의 데이터 시작위치(파일 BOM, header 를 제외하고 시작하는 위치), csv 의 column 개수를 저장한다.

static char *csv_read_one_field(CsvReader *p)
* csv데이터를 파싱해서 csv column은 CsvReader.z 에 넣어두고, CsvReader.term 에 column 다음 글자(일반적으로 콤마, newline같은 csv 구분자를 저자하고 있을 것이다. )를 저장한다. 

csvtabCreate
* 내부적으로 csvtabConnect 함수를 호출함. eponymous virtual table 이 되지 않기 위해서 따로 정의해서 사용하고 있음

​csvtabConnect
* CsvTable 메모리 할당
* schema 정의가 안 된 경우 csv 파일을 읽어서 schema 를 자동으로 만들고, 전달된 경우 해당 schema 를 이용해서 sqlite3_declare_vtab 를 호출한다. schema가 정의되어 있지않으면 실제로 csv 파일을 읽거나 생성할 때 전달받은 data 을 받아서 column 개수를 확인 한 후, c1, c2, ... 이런 형태로 column 이름을 정한다. 
* SQLITE_VTAB_DIRECTONLY (trigger 와 view 을 사용할 수 없음)을 설정한다. 

csvtabDisconnect
*
csvtabConnect 에서 할당한 메모리 해제

csvtabBestIndex
* forward full table scan 만을 지원해서 적당히 cost 값을 넣는 것 같다. 어떤 연산이든 상관없이 cost 에 1000000을 넣는다. 

csvtabOpen
* cursor(CsvCursor)에서 사용한 메모리 할당, csv 파일을 open 한다. 

csvtabClose
*
csvtabOpen 에서 사용한 메모리 해제, csv 파일을 닫는다. 

csvtabFilter
* 메모리 할당한 것들에 대해 초기화, 실질적은 동작은 csvtabNext 에서 한다. full table scan 만을 지원하기 때문에 cursor를 csv 파일의 데이터 시작위치로 옮긴다.

csvtabNext
* 하나의 column 을 가져오고 csv 파일의 row의 모든 column 이 csvCursor 에 저장된다. 
* 이 과정에서 cursor의 iRowid 를 값을 증가시키고 값이 없으면 cursor의 iRowid를 -1 로 세팅한다. 
* csv_read_one_field 가 호출되는 과정에서 자동으로 다음 row 를 가져올 수 있도록 설정될 것이다. 

csvtabColumn
* csvCursor 에 저장된 column 데이터를 sqlite3_result_text 를 호출해서 sqlite engine 에 넘겨준다. 
* 실질적으로 데이터가 넘어 가는 부분이다. 

csvtabRowid
* 현재 cursor 의 iRowid 를 리턴한다. 

csvtabEof
* 현재 cursordml iRowid 가 음수이면 eof 이다. 



딱히 뭔가 실질적으로 데이터를 필터링(sql 에서 where 하는 로직때문에 불필요한 row 자체를 전달하지 않는 동작)이 없다. 느낌으로는 sqlite engine 에 다시 뭔가 row 데이터를 검증하는 로직이 있을 것 같다. 뭔가 bloom filter 처럼 아닌 row를 제외해서 csvtabNext 가 알아서 설정하면 효율적으로 동작하고, 아니더라도 sqlite 엔진이 알아서 실제 데이터를 가지고 filtering 해주는 것으로 추정된다. 

 sqlite3 에서 소스를 수정하지 않고 sqlite3 동작을 수정할 수 있는 방법이 여러가지 있다. 

sqlite3 에 함수를 추가하는 방법(https://www.sqlite.org/c3ref/create_function.html ) , Virtual Table 을 구현하는 방법(https://www.sqlite.org/vtab.html), Virtual FileSystem을 구현하는 방법(https://www.sqlite.org/vfs.html)이있다.(더있는지는 모르겠다.)

어째든 이 방법 중 virtual table 을 구현하는 방법에 대해 공부 중이다. 내 목표는 rust 를 이용해서 vtab을 이용해서 apache arrow parquet 와 연동하고 싶다. (이미 C로 된 것은 https://github.com/cldellow/sqlite-parquet-vtable 이 있다. ). 그래서 vtab 연동하는 인터페이스에 대해 정리해 봤다. ( https://www.sqlite.org/vtab.html )

우선 vtab 을 사용하는 대표적인 경우가 csv 파일을 읽는 csv virtual table(https://www.sqlite.org/csv.html) 이다. 이것을 이용해서 csv 파일을 읽어서 table 처럼 query 를 보낼 수 있다. 

vtab 을 연동하기 위해서는 sqlite3_module  의 struct 를 모듈 생성 함수에 넘겨야 한다. sqlite3_module  구조체가 여러 함수들의 callback 함수로 구성되어 있기 때문에 이 callback 함수를 구현해야 한다. 필수 구현함수가 아닌 경우 null 을 넣는 경우수도 있다. 
함수 원형은 아래와 같이 생겼다. 

더보기

 

struct sqlite3_module {
  int iVersion;
  int (*xCreate)(sqlite3*, void *pAux,
               int argc, char *const*argv,
               sqlite3_vtab **ppVTab,
               char **pzErr);
  int (*xConnect)(sqlite3*, void *pAux,
               int argc, char *const*argv,
               sqlite3_vtab **ppVTab,
               char **pzErr);
  int (*xBestIndex)(sqlite3_vtab *pVTab, sqlite3_index_info*);
  int (*xDisconnect)(sqlite3_vtab *pVTab);
  int (*xDestroy)(sqlite3_vtab *pVTab);
  int (*xOpen)(sqlite3_vtab *pVTab, sqlite3_vtab_cursor **ppCursor);
  int (*xClose)(sqlite3_vtab_cursor*);
  int (*xFilter)(sqlite3_vtab_cursor*, int idxNum, const char *idxStr,
                int argc, sqlite3_value **argv);
  int (*xNext)(sqlite3_vtab_cursor*);
  int (*xEof)(sqlite3_vtab_cursor*);
  int (*xColumn)(sqlite3_vtab_cursor*, sqlite3_context*, int);
  int (*xRowid)(sqlite3_vtab_cursor*, sqlite_int64 *pRowid);
  int (*xUpdate)(sqlite3_vtab *, int, sqlite3_value **, sqlite_int64 *);
  int (*xBegin)(sqlite3_vtab *pVTab);
  int (*xSync)(sqlite3_vtab *pVTab);
  int (*xCommit)(sqlite3_vtab *pVTab);
  int (*xRollback)(sqlite3_vtab *pVTab);
  int (*xFindFunction)(sqlite3_vtab *pVtab, int nArg, const char *zName,
                     void (**pxFunc)(sqlite3_context*,int,sqlite3_value**),
                     void **ppArg);
  int (*Rename)(sqlite3_vtab *pVtab, const char *zNew);
  /* The methods above are in version 1 of the sqlite_module object. Those 
  ** below are for version 2 and greater. */
  int (*xSavepoint)(sqlite3_vtab *pVTab, int);
  int (*xRelease)(sqlite3_vtab *pVTab, int);
  int (*xRollbackTo)(sqlite3_vtab *pVTab, int);
  /* The methods above are in versions 1 and 2 of the sqlite_module object.
  ** Those below are for version 3 and greater. */
  int (*xShadowName)(const char*);
};


xCreate : int (*xCreate)(sqlite3 *db, void *pAux, int argc, char *const*argv, sqlite3_vtab **ppVTab, char **pzErr);
xConnect :
int (*xConnect)(sqlite3*, void *pAux, int argc, char *const*argv, sqlite3_vtab **ppVTab, char **pzErr);
* xCreate  는 생성함수로 vtab 이 생성될 때 호출된다. SQL 구문  CREATE VIRTUAL TABLE 테이블명 ....     형식이다. 
xCreate와 xConnect 는 유사하다. xCrete 는 생성될 때 이용되고, xConnect 는 생성된 가상테이블에 대해 불러올 때 사용된다. xCrete 와 xConnect 를 동일한 callback 함수(동일한 함수포인터)를 넣을 수도 있는데, 이렇게 하면 Eponymous virtual tables(CREATE VIRTUAL TABLE 문 없이 미리 로딩된 경우) 이 된다. xCreate 가 null pointer 이면 eponymous-only virtual table(CREATE VIRTUAL TABLE 문을 사용할 수 없고, table-valued functions 전용, table-valued functions 은 table 을 return 하는 sql 함수로 generate_series 함수 같이 일반 함수인데, table row 를 리턴해서 테이블 처럼 SELECT value FROM generate_series(5,50); 으로 사용 가능 )
csv virtual table 경우 xCrete 내부적으로 xConnect 를 호출하고 있다. 
* 꼭 이들 함수 안에서 sqlite3_declare_vtab 를 호출해서 가상테이블의 column 과 datatype 을 알려야 한다. 일반 database 의 table 을 create 하듯이 sql 쿼리를 전달해야한다. 필수 구현 사항이다.
* 또 sqlite3_vtab_config 를 호출해서 SQLITE_VTAB_CONSTRAINT_SUPPORT , SQLITE_VTAB_INNOCUOUS, SQLITE_VTAB_DIRECTONLY  중 하나를 선택해서 가상 테이블에 대한 환경설정을 할 수도 있다. 이것은 옵션이라서 호출 안해도 상관 없어 보인다. SQLITE_VTAB_INNOCUOUS(trigger 와 view를 악의적으로 사용되더라도 피해를 줄 수 없음),   SQLITE_VTAB_DIRECTONLY(trigger 와 view를 사용금지), SQLITE_VTAB_CONSTRAINT_SUPPORT(2번째 argument 도 활용해서 좀 복잡하다. )

xBestIndex  (int (*xBestIndex)(sqlite3_vtab *pVTab, sqlite3_index_info*);)
* 연산에 대해 처리 cost 를 계산해서 효율적인 접근 방법을 계산할 때 이용한다. sqlite3_prepare 또는 유사한 기능(아마 쿼리를 직접 호출하는 경우를 말하는 것 같다.)을 이용할 때, 이 callback 함수가 호출되며, 여러번 호출될 수 있다. 그러면서 최적의 속도를 얻을 수 있는 방법을 계산한다. 이 함수가 구현의 핵심부분이라 더 자세히 찾아볼 필요가 있다. 

xDisconnect : int (*xDisconnect)(sqlite3_vtab *pVTab);

xDestroy
: int (*xDestroy)(sqlite3_vtab *pVTab);
* xDisconnect 는 삭제가 아닌 연결만을 해제합니다. xDisconnect 는 DB연결이 close 될 때 호출되고, xDestroy 는  DROP TABLE 구문이 실행될 때, 호출된다. csv virtual table 경우 둘 다 동일한 callback point 이다. 

xOpen : int (*xOpen)(sqlite3_vtab *pVTab, sqlite3_vtab_cursor **ppCursor);
xClose
: int (*xClose)(sqlite3_vtab_cursor*);
* xOpen는 virtual table 에 접근하는(read / write) 하는 cursor를 생성한다. xClose는 이 cursor 를 close 한다. xOpen 은 모든 virtual table 에 필수적으로 필요한 callback 이다. 이렇게 해서 생성된 cursor는 cursor의 위치를 지정하거(file seek의 의미인듯) read 하기 전에 xFilter 를 호출한다. 

xEof : int (*xEof)(sqlite3_vtab_cursor*);
* 현재 cursor 가 유효한 레코드를 가리키고 false, 아니면 true 를 리턴한다. xEof 는 xNext 나 xFilter 호출후에 호출된다.

xFilter : int (*xFilter)(sqlite3_vtab_cursor*, int idxNum, const char *idxStr, int argc, sqlite3_value **argv);
* xOpen에 의해 열린 cursor 에 대해 검색을 함. 필터 기준에 만족하는 레코드가 있으면 커서를 해당 레코드에 위치시킨다. 검색 조건은 argc, argv에 의해 전달되는데, 이 값은 xBestIndex  함수가 호출될 때, 설정된 값으로 전달되는 것 같다. (정확히 어떻게 전달되는지 확인필요). 

xNext : int (*xNext)(sqlite3_vtab_cursor*);
* xFilter 에 의해 cursor 가 세팅된 이후, 값이 불리고 나서 다음 record가 호출될 때, xNext 가 불러지는 것 같다. 

xColumn : int (*xColumn)(sqlite3_vtab_cursor*, sqlite3_context*, int N);
* 현재 행의 N번째 열에 대한 값을 찾기 위해 이 메서드를 호출합니다

xRowid : int (*xRowid)(sqlite3_vtab_cursor *pCur, sqlite_int64 *pRowid);
* 현재 cursor에 대한 rowID 를 pRowid 로 전달한다. 

xUpdate : int (*xUpdate)(sqlite3_vtab *pVTab, int argc, sqlite3_value **argv, sqlite_int64 *pRowid);
* 가상테이블에 대해 delete, insert, update 쿼리를 보낼 때, 호출된다. 가상테이블에 대한 수정을 지원하지 않는 경우 구현하지 않아도 된다. 

xFindFunction : int (*xFindFunction)(sqlite3_vtab *pVtab, int nArg, const char *zName,
  void (**pxFunc)(sqlite3_context*,int,sqlite3_value**), void **ppArg);
* sqlite3_prepare 호출될 때, 가상 테이블 구현에 대해 함수를 overload 할 기회를 제공한다. 여기서는 virtual table 에 대해서만 생각했지만, sqlite 에서 모듈을 이용해서 SQL 함수를 구현할 수 있다. 이렇게 구현한 SQL 함수에 대해 다시 재 정의가 가능한 것 같다. 

xBegin : int (*xBegin)(sqlite3_vtab *pVTab);
xSync : int (*xSync)(sqlite3_vtab *pVTab);
xCommit : int (*xCommit)(sqlite3_vtab *pVTab);
xRollback : int (*xRollback)(sqlite3_vtab *pVTab);
* 가상 테이블에 대해 transaction을 구현할 때, callback 이 호출됨

xSavepoint : int (*xSavepoint)(sqlite3_vtab *pVtab, int);
xRelease : int (*xRelease)(sqlite3_vtab *pVtab, int);
xRollbackTo : int (*xRollbackTo)(sqlite3_vtab *pVtab, int);
* transaction 에서 더 나가 savepoint 기능을 구현할 때 필요

xRename : int (*xRename)(sqlite3_vtab *pVtab, const char *zNew);
* alter table 을 구현할 때 필요


 

 


http://code.google.com/p/multidbviewer/

구글 코드에 multidbviewer 라는 프로젝트를 생성했습니다. 약 2년간 진행해온 프로젝트 인데 드디오 공개 했습니다.  프로그램은 다양한 db 의 QueryBrowser를 만드는 프로젝트 입니다. 현재 sqlite 에 대해서만 동작하고 있고 좀 버그가 많은 상태입니다. 좀 고쳐야 하는 버그가 많은지라 공개를 할까 말까 고민했었는데, 이러다가는 영영 공개 못할 거라느 생각에 과감히 공개 했습니다. 

 재 블로그에 보면 유난히 wxwidget 과 sqlite 관련 글들이 많은데, 저 프로젝트를 진행하면서 필요한 기술을 습득하면서 올린 글들입니다. 그래서 저 프로그램의 GUI 는 wxwidget 입니다. 제
블로그에 visual studio 나 codeblock 으로 wxwdiget을 컴파일하고 프로젝트를 만드는 것이 나오는데 저 프로젝트 또한 windows 에서 vistual studo 와 mingw+codeblock 조합으로 컴파일 가능하고 ubuntu 에서 codeblock 으로 컴파일 가능합니다. 
 아직 osx 에서 codeblock 이나 xcode 로 wxwidget 을 컴파일 하는 방법을 찾지 못해 mac 에 대한 지원은 업습니다. 콘솔에서 컴파일은 해봤지만 제 프로그램 콘셉이 GUI에서 컴파일 가능한 프로젝트를 구성하는 것도 포함되어 있기 때문에 더 이상 진행하지 않고 있는 상태입니다.


 라이센스는 MPL1.1로 했습니다. 여기에 면책조항이 있습니다. 저 프로그램으로 뭔가 문제가 생겨도 제가 책임을 질 수 없기 때문에 저에게는 중요한 요소 입니다. 이말은 즉, 저 프로그램을 그냥 테스트 용도로만 사용하기 바랍니다. 

  다음 버전에는 cubrid 로 지원하려고 합니다. 현재도 일부 cubrid 지원코드가 들어가 있기는 한데 #define 으로 막아둔 상태입니다.  cubrid 를 지원한다고 해도 그냥 queryBrowser 기능만을 추가할 계획입니다.  뭐, user 를 생성하고 한다든지, 권한을 준다든지는 고려하지 않을 계획입니다. 


 wxwidget을 공부하는데는 꽤 괜찮은 구조라고 자부합니다. wxFromBuilder 을 이용해 화면을 구성했는데 원본 파일도 들어 있고, 툴바 이미지 파일도 들어 있습니다. wxwidet 프로그램을 할 수 있는 환경이라면 컴파일 가능한 코드 입니다. 




 현재 버전은 0.1 이고, 쓸만해질 때 까지는 1.0 으로 올리지 않을 계획입니다. 언제 1.0이 될 수 있을 지 모르겠군요..