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 을 구현할 때 필요


 

 

vscode 로 msvc 2017 로 빌드하는 환경을 구축했다. 이게 의외로 좀 어려워서 아래에 정리한다. 참고한 문서는 https://code.visualstudio.com/docs/cpp/config-msvc이다. 

기본 환경 : 당연한 Visual  Visual Studio 2017 이 설치되어야 한다. 내 경우는 community 버전이다.
vscode 의 경우 확장 Microsoft C/C++ extension 이 설치되어야 한다. ( https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools )

1. batch 파일로 vscode 여는 환경 구축
 visual studio 컴파일러 동작시 여러 환경 변수가 설정되어야 한다. (참고 : https://docs.microsoft.com/ko-kr/dotnet/csharp/language-reference/compiler-options/how-to-set-environment-variables-for-the-visual-studio-command-line) 이 것을 쉽게 batch 파일로 할 수 있다. 이 파일이 VsDevCmd.bat 이라는 파일이다. 내 경우에는 이 파일이 C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\Tools\VsDevCmd.bat 에 존재한다. 이 것은 각자 Visual Studio 2017  경로에 따라 다르거나 community 버전 인지 일반 구매 버전인지에 따라 다를 수 있다. 이 파일을 실행하고 나서 vscode 를 실행해야 한다. 이게 프로그램을 실행할 때마다 이렇게 한다는게 참 귀찮다. 따라서 이 것을 미리 batch 파일로 만들어 두면 편한다. 
 내 경우 아래와 같이 만들어 두었다.

call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\Tools\VsDevCmd.bat"  -arch=x64
D:
cd D:\workspace\vscode2\hello_world
code .

-arch=x64 는 64bit 로 환경 설정하기 위해 설정되었다. 이 배치 파일이 C 드라이버이기 때문에 D에 접속 하려는  D: 이라고 필요하다. 그냥 cd 를 하면 접근할 수 없다.  code .  이라는 코드가 vscode 를 현재 디렉토리를 working directory 로 실행하는 명령어 이다. 당연이 hello_world 는 미리 폴더가 만들어 져야 한다.

2. helloworld.cpp
컴파일 하려는 파일은 helloworld.cpo 파일이다.

#include <iostream>
#include <vector>
#include <string>

using namespace std;

int main()
{

    vector<string> msg {"Hello", "C++", "World", "from", "VS Code!"};

    for (const string& word : msg)
    {
        cout << word << " ";
    }
    cout << endl;
}



3. vscode 빌드 환경 구축
프로젝트 디렉토리 안의 .vscode 내에 프로젝트별 환경을 설정할 수 있다. 설정해야 하는 파일은 아래와 같다. 

.vscode\c_cpp_properties.json  (compiler path and IntelliSense settings)

{
    "configurations": [
      {
        "name": "Win32",
        "defines": ["_DEBUG", "UNICODE", "_UNICODE"],
        "compilerPath": "C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC/Tools/MSVC/14.16.27023/bin/Hostx64/x64/cl.exe",
        "windowsSdkVersion": "10.0.17763.0",
        "intelliSenseMode": "msvc-x64",
        "cStandard": "c11",
        "cppStandard": "c++17"
      }
    ],
    "version": 4
  }

 

.vscode\tasks.json (build instructions) (실제로 build 하는 command 이다. )

{
    "version": "2.0.0",
    "tasks": [
      {
        "label": "msvc build",
        "type": "shell",
        "command": "cl.exe",
        "args": ["/EHsc", "/Zi", "/Fe:", "helloworld.exe", "helloworld.cpp"],
        "group": {
          "kind": "build",
          "isDefault": true
        },
        "presentation": {
          "reveal": "always"
        },
        "problemMatcher": "$msCompile"
      }
    ]
  }


.vscode\launch.json (debugger settings)

{
    "version": "0.2.0",
    "configurations": [
      {
        "name": "(msvc) Launch",
        "type": "cppvsdbg",
        "request": "launch",
        "program": "${workspaceFolder}/helloworld.exe",
        "args": [],
        "stopAtEntry": true,
        "cwd": "${workspaceFolder}",
        "environment": [],
        "externalConsole": false
      }
    ]
  }

 

4. 빌드하기
빌드를 하려는 위에서 설정한 tasks.json 의 msvc build 를 실행하면 된다. vscode 에서 task 를 실행하는 방법은 ctrl + shift + p  를 누르면 아래와 같은 창이 뜬다. 이 중 "작업: 빌드 작업 실행"을 선택하면 된다. 

여기에서는 가장 첫번째 항목이다.

혹시 이 다음에도 추가로 선택해야 하는 화면이 있다면 cl.exe 를 선택하기 바란다. 

4-1. 빌드 하다가 에러가 발생한다면 보통 batch 파일 설정이 잘못된 것이라는 의심이 든다. 특히 include 경로가 없다고 에러가 발생한다면 더더욱 해당 case 가 의심스럽다.
4-2. cl.exe 가 동작하다가 없다고 에러가 발생했다면 batch 파일 환경에서 cl.exe 가 실행되는지 확인 하기 바란다.

5. 단축키 설정하기
빌드 하는 환경이 단축키가 없어서 너무 불편하다. 이 경우에는 단축키를 설정하도록 한다. vscode 에서 단축키는 프로젝트 별로 설정하는 방법이 없다면 전체 단축키를 설정해야 한다. 
파일 -> 기본설정 -> 바로가기 키  를 누르면 설정할 수 있다. workbench.action.tasks.runTask 를 선택한 후 단축키를 설정할 수 있다. 해당 항목을 더블클릭하고 원하는 대로 단축키를 입력하면 설정할 수 있다. 내 경우에는 ctrl+alt+b  로 설정했다. 이 때 아래 그림에서 붉은 색 부분을 클릭하면 json 으로 수정할 수 있다.

keybindings.json 파일로 프로그램 전체 단축키이다. 

이 때 단순히 task 의 실행뿐만 아니라 task 중에서 특정 task 를 지정해서 실행할 수 있다. 

// 키 바인딩을 이 파일에 넣어서 기본값 재정의auto[]
[
    {
        "key": "ctrl+alt+b",
        "command": "workbench.action.tasks.runTask",
        "args": "msvc build"
    }
]

args 이름이 중요한데, 이 이름으 "msvc build" 로 되어 있다. 위에서 tasks.json 의 label 을 선택할 수 있다. 이 방식을 잘 활용하면 빌드 clean 이라는 task 를 만들고, 단축키로 buld clean 을 만들 수도 있다. 

6. 디버깅 하기
위에서 이미 launch.json 파일을 만들었기 때문에 단축키 F5 를 눌러서 하면된다.



몇가지 추가 설정이 필요할 것 같다. 뭔가 release 버전으로 build 한다던지, 임시파일이나 실행파일이 따로 output 폴더로 지정되어야 한다든지의 작업이 추가로 설정이 필요할 것 같다. 어쩌면 좀 복잡한 프로그램은 그냥, Visual Studio 를 사용하는게 답일지도 모르겠다. gcc 를 이용한 경우 vscode 가 이점이 있을 것 같은데, msvc 라면 그냥 visual studio 가 더 편할 것 같다.