SQLite의 가상 테이블 메커니즘

SQLite의 가상 테이블 메커니즘 (The Virtual Table Mechanism Of SQLite)

SQLite의 가상 테이블(virtual table) 메커니즘, 즉 가상 테이블을 만들고 사용하는 방법과 각 가상 테이블 메서드(xCreate, xConnect, xBestIndex 등)의 역할을 상세히 설명하는 문서예요.

출처: The Virtual Table Mechanism Of SQLite (sqlite.org)

본문

1. 소개 (Introduction)

가상 테이블은 열린 SQLite 데이터베이스 연결에 등록되는 객체예요. SQL 문의 관점에서 가상 테이블 객체는 다른 테이블이나 뷰처럼 보여요. 하지만 그 뒤에서는 가상 테이블에 대한 쿼리와 갱신이 데이터베이스 파일을 읽고 쓰는 대신 가상 테이블 객체의 콜백 메서드를 호출해요.

가상 테이블 메커니즘은 응용 프로그램이 SQL 문에서 테이블인 것처럼 접근 가능한 인터페이스를 공개할 수 있게 해줘요. SQL 문은 다음 예외를 제외하고 실제 테이블에 할 수 있는 거의 모든 것을 가상 테이블에 할 수 있어요:

  • 가상 테이블에는 트리거(trigger)를 만들 수 없어요.
  • 가상 테이블에는 추가 색인을 만들 수 없어요. (가상 테이블은 색인을 가질 수 있지만 그것은 가상 테이블 구현에 내장되어야 해요. CREATE INDEX 문으로 별도로 추가할 수는 없어요.)
  • 가상 테이블에는 ALTER TABLE ... ADD COLUMN 명령을 실행할 수 없어요.

개별 가상 테이블 구현은 추가 제약을 부과할 수 있어요. 예를 들어 일부 가상 구현은 읽기 전용 테이블을 제공할 수 있어요. 또는 일부 가상 테이블 구현은 INSERTDELETE는 허용하지만 UPDATE는 허용하지 않을 수 있어요. 또는 일부 가상 테이블 구현은 만들 수 있는 UPDATE의 종류를 제한할 수 있어요.

가상 테이블은 인메모리 데이터 구조를 나타낼 수 있어요. 또는 SQLite 형식이 아닌 디스크의 데이터 뷰를 나타낼 수 있어요. 또는 응용 프로그램이 요청 시 가상 테이블의 내용을 계산할 수 있어요.

다음은 가상 테이블의 기존 및 추정된 용도 몇 가지예요:

  • 전문 검색(full-text search) 인터페이스
  • R-Trees를 사용한 공간 색인
  • SQLite 데이터베이스 파일의 디스크 내용 검사(내부 조사, dbstat 가상 테이블)
  • CSV(comma-separated value) 파일의 내용 읽기 및/또는 쓰기
  • 호스트 컴퓨터의 파일 시스템을 데이터베이스 테이블처럼 접근
  • R 같은 통계 패키지의 데이터에 SQL 조작 가능하게 하기

실제 가상 테이블 구현의 더 긴 목록은 가상 테이블 목록 페이지를 참고하세요.

1.1. 사용법 (Usage)

가상 테이블은 CREATE VIRTUAL TABLE 문으로 만들어져요.

CREATE VIRTUAL TABLE IF NOT EXISTS schema-name . table-name USING module-name ( module-argument ) ,

CREATE VIRTUAL TABLE 문은 module-name 클래스에서 파생된 table-name이라는 새 테이블을 만들어요. module-name은 sqlite3_create_module() 인터페이스가 가상 테이블에 등록한 이름이에요.

CREATE VIRTUAL TABLE tablename USING modulename;

모듈 이름 뒤에 쉼표로 구분된 인자를 모듈에 제공할 수도 있어요:

CREATE VIRTUAL TABLE tablename USING modulename(arg1, arg2, ...);

모듈 인자의 형식은 매우 일반적이에요. 각 module-argument는 키워드, 문자열 리터럴, 식별자, 숫자, 구두점을 포함할 수 있어요. 각 module-argument는 작성된 대로(텍스트로) 가상 테이블이 생성될 때 가상 테이블 구현의 생성자 메서드에 전달되며, 그 생성자가 인자를 파싱하고 해석할 책임이 있어요. 인자 구문은 충분히 일반적이라 가상 테이블 구현은 원하면 인자를 일반 CREATE TABLE 문의 열 정의로 해석할 수 있어요. 구현은 인자에 다른 해석을 부과할 수도 있어요.

가상 테이블이 생성되면 위에서 언급하고 특정 가상 테이블 구현이 부과하는 예외를 제외하고 다른 테이블처럼 사용할 수 있어요. 가상 테이블은 일반 DROP TABLE 구문으로 파괴돼요.

1.1.1. 임시 가상 테이블 (Temporary virtual tables)

"CREATE TEMP VIRTUAL TABLE" 문은 없어요. 임시 가상 테이블을 만들려면 가상 테이블 이름 앞에 "temp" 스키마를 추가해요.

CREATE VIRTUAL TABLE temp.tablename USING module(arg1, ...);

1.1.2. 동명 가상 테이블 (Eponymous virtual tables)

일부 가상 테이블은 CREATE VIRTUAL TABLE 문 없이도, 그 모듈이 등록된 모든 데이터베이스 연결의 "main" 스키마에 자동으로 존재해요. 그런 가상 테이블을 "동명 가상 테이블(eponymous virtual table)"이라고 불러요. 동명 가상 테이블을 사용하려면 모듈 이름을 테이블인 것처럼 쓰면 돼요. 동명 가상 테이블은 "main" 스키마에만 존재하므로, 다른 스키마 이름을 접두사로 붙이면 동작하지 않아요.

동명 가상 테이블의 예는 dbstat 가상 테이블이에요. dbstat 가상 테이블을 동명 가상 테이블로 사용하려면 "dbstat" 모듈 이름을 일반 테이블인 것처럼 쿼리하면 돼요. (SQLite를 SQLITE_ENABLE_DBSTAT_VTAB 옵션으로 컴파일해야 빌드에 dbstat 가상 테이블이 포함된다는 점에 주의하세요.)

SELECT * FROM dbstat;

xCreate 메서드가 xConnect 메서드와 정확히 같은 함수이거나 xCreate 메서드가 NULL이면 가상 테이블은 동명(eponymous)이에요. xCreate 메서드는 CREATE VIRTUAL TABLE 문으로 가상 테이블이 처음 생성될 때 호출돼요. xConnect 메서드는 데이터베이스 연결이 스키마에 연결(attach)하거나 다시 파싱할 때마다 호출돼요. 이 두 메서드가 같으면 가상 테이블이 생성·파괴해야 할 영구 상태가 없음을 나타내요.

1.1.3. 동명 전용 가상 테이블 (Eponymous-only virtual tables)

xCreate 메서드가 NULL이면 그 가상 테이블에 대해 CREATE VIRTUAL TABLE 문이 금지되고, 가상 테이블은 "동명 전용 가상 테이블(eponymous-only virtual table)"이에요. 동명 전용 가상 테이블은 테이블 값 함수로 유용해요.

3.9.0 버전(2015-10-14) 이전에는 SQLite가 xCreate 메서드를 호출하기 전에 NULL인지 확인하지 않았다는 점에 주의하세요. 따라서 동명 전용 가상 테이블이 3.8.11.1 버전(2015-07-29) 이하의 SQLite에 등록되고 그 가상 테이블 모듈에 CREATE VIRTUAL TABLE 명령이 시도되면 NULL 포인터로 점프해 충돌(crash)이 발생해요.

1.2. 구현 (Implementation)

가상 테이블 구현에는 여러 새 C 수준 객체가 사용돼요:

typedef struct sqlite3_vtab sqlite3_vtab;
typedef struct sqlite3_index_info sqlite3_index_info;
typedef struct sqlite3_vtab_cursor sqlite3_vtab_cursor;
typedef struct sqlite3_module sqlite3_module;

sqlite3_module 구조체는 가상 테이블을 구현하는 데 사용되는 모듈 객체를 정의해요. 모듈을 비슷한 속성을 가진 여러 가상 테이블을 만들 수 있는 클래스로 생각하세요. 예를 들어 디스크의 CSV(comma-separated-value) 파일에 읽기 전용 접근을 제공하는 모듈이 있을 수 있어요. 그 하나의 모듈로 각각 다른 CSV 파일을 가리키는 여러 가상 테이블을 만들 수 있어요.

모듈 구조체는 SQLite가 가상 테이블에 대해 다양한 작업(새 가상 테이블 인스턴스 생성 또는 이전 것 파괴, 데이터 읽기와 쓰기, 행 검색과 삭제, 갱신, 삽입)을 수행하기 위해 호출하는 메서드를 담아요. 모듈 구조체는 아래에서 더 자세히 설명돼요.

각 가상 테이블 인스턴스는 sqlite3_vtab 구조체로 표현돼요. sqlite3_vtab 구조체는 다음과 같아요:

struct sqlite3_vtab {
  const sqlite3_module *pModule;
  int nRef;
  char *zErrMsg;
};

가상 테이블 구현은 보통 이 구조체를 하위 클래스화해 추가 개인 및 구현 특정 필드를 더할 거예요. nRef 필드는 SQLite 코어가 내부적으로 사용하며 가상 테이블 구현이 변경해서는 안 돼요. 가상 테이블 구현은 zErrMsg에 오류 메시지 문자열을 넣어 코어에 오류 메시지 텍스트를 전달할 수 있어요. 이 오류 메시지 문자열을 담을 공간은 sqlite3_mprintf()sqlite3_malloc() 같은 SQLite 메모리 할당 함수에서 얻어야 해요. zErrMsg에 새 값을 할당하기 전에 가상 테이블 구현은 sqlite3_free()로 zErrMsg의 기존 내용을 해제해야 해요. 이렇게 하지 않으면 메모리 누수가 발생해요. SQLite 코어는 오류 메시지 텍스트를 클라이언트 응용 프로그램에 전달하거나 가상 테이블을 파괴할 때 zErrMsg의 내용을 해제하고 0으로 만들 거예요. 가상 테이블 구현은 zErrMsg 내용을 새롭고 다른 오류 메시지로 덮어쓸 때만 그 내용 해제에 신경 쓰면 돼요.

sqlite3_vtab_cursor 구조체는 가상 테이블의 특정 행에 대한 포인터를 나타내요. sqlite3_vtab_cursor는 다음과 같아요:

struct sqlite3_vtab_cursor {
  sqlite3_vtab *pVtab;
};

다시 한 번, 실제 구현은 이 구조체를 하위 클래스화해 추가 개인 필드를 더할 가능성이 커요.

sqlite3_index_info 구조체는 가상 테이블을 구현하는 모듈의 xBestIndex 메서드에 정보를 전달하고 그로부터 정보를 받는 데 사용돼요.

CREATE VIRTUAL TABLE 문을 실행하기 전에, 그 문에 지정된 모듈이 데이터베이스 연결에 등록되어 있어야 해요. 이것은 sqlite3_create_module() 또는 sqlite3_create_module_v2() 인터페이스 중 하나로 이루어져요:

int sqlite3_create_module(
  sqlite3 *db,               /* SQLite connection to register module with */
  const char *zName,         /* Name of the module */
  const sqlite3_module *,    /* Methods for the module */
  void *                     /* Client data for xCreate/xConnect */
);
int sqlite3_create_module_v2(
  sqlite3 *db,               /* SQLite connection to register module with */
  const char *zName,         /* Name of the module */
  const sqlite3_module *,    /* Methods for the module */
  void *,                    /* Client data for xCreate/xConnect */
  void(*xDestroy)(void*)     /* Client data destructor function */
);

sqlite3_create_module()sqlite3_create_module_v2() 루틴은 모듈 이름을 sqlite3_module 구조체와 각 모듈에 특정한 별도의 클라이언트 데이터 포인터와 연관지어요. 두 create_module 메서드의 유일한 차이는 _v2 메서드가 클라이언트 데이터 포인터에 대한 소멸자를 지정하는 추가 매개변수를 포함한다는 것이에요. 모듈 구조체는 가상 테이블의 동작을 정의해요. 모듈 구조체는 다음과 같아요:

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 (*xRename)(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*);
  /* The methods above are in versions 1 through 3 of the sqlite_module object.
  ** Those below are for version 4 and greater. */
  int (*xIntegrity)(sqlite3_vtab *pVTab, const char *zSchema,
                    const char *zTabName, int mFlags, char **pzErr);
};

모듈 구조체는 각 가상 테이블 객체에 대한 모든 메서드를 정의해요. 모듈 구조체는 또한 모듈 테이블 구조체의 특정 판(edition)을 정의하는 iVersion 필드를 포함해요. 현재 iVersion은 항상 4 이하이지만, 미래의 SQLite 릴리스에서 모듈 구조체 정의가 추가 메서드로 확장될 수 있고 그 경우 최대 iVersion 값이 증가할 거예요.

모듈 구조체의 나머지는 가상 테이블의 다양한 기능을 구현하는 데 사용되는 메서드로 구성돼요. 각 메서드가 무엇을 하는지에 대한 세부 사항은 다음 절에서 제공돼요.

1.3. 가상 테이블과 공유 캐시 (Virtual Tables And Shared Cache)

SQLite 3.6.17 버전(2009-08-10) 이전에는 가상 테이블 메커니즘이 각 데이터베이스 연결이 데이터베이스 스키마의 자체 사본을 유지한다고 가정했어요. 따라서 공유 캐시 모드가 활성화된 데이터베이스에서는 가상 테이블 메커니즘을 사용할 수 없었어요. sqlite3_create_module() 인터페이스는 공유 캐시 모드가 활성화되면 오류를 반환했어요. 그 제한은 SQLite 3.6.17 버전부터 완화되었어요.

1.4. 새 가상 테이블 구현 만들기 (Creating New Virtual Table Implementations)

자신만의 가상 테이블을 만들려면 다음 단계를 따르세요:

  1. 필요한 모든 메서드를 작성한다.
  2. 1단계의 모든 메서드에 대한 포인터를 담은 sqlite3_module 구조체 인스턴스를 만든다.
  3. sqlite3_create_module() 또는 sqlite3_create_module_v2() 인터페이스 중 하나로 sqlite3_module 구조체를 등록한다.
  4. USING 절에 새 모듈을 지정하는 CREATE VIRTUAL TABLE 명령을 실행한다.

정말 어려운 부분은 1단계뿐이에요. 기존 가상 테이블 구현에서 시작해 필요에 맞게 수정하고 싶을 수 있어요. SQLite 소스 트리에는 복사하기에 적합한 많은 가상 테이블 구현이 있어요, 예를 들면:

  • templatevtab.c → 다른 맞춤 가상 테이블의 템플릿 역할을 하도록 특별히 만들어진 가상 테이블.
  • series.c → generate_series() 테이블 값 함수의 구현.
  • json.cjson_each()json_tree() 테이블 값 함수의 소스를 담고 있음.
  • csv.c → CSV 파일을 읽는 가상 테이블.

SQLite 소스 트리에는 예로 사용할 수 있는 다른 많은 가상 테이블 구현이 있어요. "sqlite3_create_module"을 검색해 이 다른 가상 테이블 구현들을 찾아보세요.

새 가상 테이블을 로더블 확장(loadable extension)으로 구현하는 것도 고려할 수 있어요.

1.5. 보안 고려 사항 (Security Considerations)

맞춤 가상 테이블은 신중하게 구현하지 않으면 보안 취약점이 될 수 있어요. 문제를 방지하기 위해 SQLite는 가상 테이블에 대해 다음과 같은 추가 보안 기능을 제공해요:

  1. xConnect 및/또는 xCreate 메서드는 SQLITE_VTAB_DIRECTONLY 옵션으로 sqlite3_vtab_config()를 호출해 가상 테이블이 트리거나 뷰 안에서 사용되는 것을 막을 수 있어요. 트리거나 뷰 안에서 가상 테이블을 사용할 필요가 없다면 이렇게 하는 것이 좋아요.
  2. 꼭 필요한 경우가 아니면 SQLITE_VTAB_INNOCUOUS 옵션으로 sqlite3_vtab_config()를 사용하는 것을 피하세요. 가상 테이블에 부작용이 없고 그 설정 없이는 목적을 달성할 수 없다고 확신할 때만 SQLITE_VTAB_INNOCUOUS를 설정하세요.
  3. 가상 테이블 자체가 SQL 문을 실행하고 그 문이 가상 테이블을 만든 CREATE VIRTUAL TABLE 문의 인자에 의존할 때는 SQLITE_PREPARE_FROM_DDL 옵션으로 sqlite3_prepare_v3() 인터페이스를 사용해 SQLITE_DBCONFIG_TRUSTED_SCHEMA 설정의 우회를 막아요. SQL 문이 CREATE VIRTUAL TABLE 문에 의존하는지 확실하지 않으면(가상 테이블의 의미론에 따라) 꼭 필요하지 않아도 SQLITE_PREPARE_FROM_DDL을 포함해도 보통 무해하므로, 특별한 이유가 없으면 기본 동작은 SQLITE_PREPARE_FROM_DDL을 사용하는 것이 좋아요.

맞춤 SQL 함수의 보안 영향어둠의 예술에 대한 방어도 참고하세요.

2. 가상 테이블 메서드 (Virtual Table Methods)

2.1. xCreate 메서드

int (*xCreate)(sqlite3 *db, void *pAux,
             int argc, char *const*argv,
             sqlite3_vtab **ppVTab,
             char **pzErr);

xCreate 메서드는 CREATE VIRTUAL TABLE 문에 응답해 가상 테이블의 새 인스턴스를 만들기 위해 호출돼요. xCreate 메서드가 xConnect 메서드와 같은 포인터라면 가상 테이블은 동명 가상 테이블이에요. xCreate 메서드가 생략되면(NULL 포인터이면) 가상 테이블은 동명 전용 가상 테이블이에요.

db 매개변수는 CREATE VIRTUAL TABLE 문을 실행 중인 SQLite 데이터베이스 연결에 대한 포인터예요. pAux 인자는 가상 테이블 모듈을 등록한 sqlite3_create_module() 또는 sqlite3_create_module_v2() 호출의 네 번째 인자였던 클라이언트 데이터 포인터의 사본이에요. argv 매개변수는 argc개의 0으로 끝나는 문자열에 대한 포인터 배열이에요. 첫 번째 문자열 argv[0]은 호출되는 모듈의 이름이에요. 모듈 이름은 sqlite3_create_module()의 두 번째 인자로, 그리고 실행 중인 CREATE VIRTUAL TABLE 문의 USING 절 인자로 제공된 이름이에요. 두 번째 argv[1]은 새 가상 테이블이 생성되는 데이터베이스의 이름이에요. 데이터베이스 이름은 기본 데이터베이스의 경우 "main", TEMP 데이터베이스의 경우 "temp", 연결된(attached) 데이터베이스의 경우 ATTACH 문 끝에 주어진 이름이에요. 배열의 세 번째 요소 argv[2]는 CREATE VIRTUAL TABLE 문에서 TABLE 키워드 뒤에 지정된 새 가상 테이블의 이름이에요. 있으면 argv[] 배열의 네 번째 및 이후 문자열이 CREATE VIRTUAL TABLE 문의 모듈 이름에 대한 인자를 보고해요.

이 메서드의 역할은 새 가상 테이블 객체(sqlite3_vtab 객체)를 구성하고 *ppVTab에 그 포인터를 반환하는 것이에요.

sqlite3_vtab 구조체를 만드는 작업의 일부로, 이 메서드는 반드시 sqlite3_declare_vtab()을 호출해 가상 테이블의 열과 데이터 타입에 대해 SQLite 코어에 알려야 해요. sqlite3_declare_vtab() API의 프로토타입은 다음과 같아요:

int sqlite3_declare_vtab(sqlite3 *db, const char *zCreateTable)

sqlite3_declare_vtab()의 첫 번째 인자는 이 메서드의 첫 번째 매개변수와 같은 데이터베이스 연결 포인터여야 해요. sqlite3_declare_vtab()의 두 번째 인자는 가상 테이블의 열과 그 데이터 타입을 정의하는 잘 구성된(well-formed) CREATE TABLE 문을 담은 0으로 끝나는 UTF-8 문자열이어야 해요. 이 CREATE TABLE 문의 테이블 이름은 모든 제약과 함께 무시돼요. 열 이름과 데이터 타입만 중요해요. CREATE TABLE 문 문자열을 영구 메모리에 보관할 필요는 없어요. sqlite3_declare_vtab() 루틴이 반환하는 즉시 문자열을 할당 해제하고/하거나 재사용할 수 있어요.

xConnect 메서드는 sqlite3_vtab_config() 인터페이스를 한 번 이상 호출해 가상 테이블에 대한 특수 기능을 선택적으로 요청할 수도 있어요:

int sqlite3_vtab_config(sqlite3 *db, int op, ...);

sqlite3_vtab_config() 호출은 선택 사항이에요. 하지만 최대 보안을 위해, 가상 테이블이 트리거나 뷰 안에서 사용되지 않을 것이라면 가상 테이블 구현은 "sqlite3_vtab_config(db, SQLITE_VTAB_DIRECTONLY)"을 호출하는 것이 좋아요.

xCreate 메서드는 sqlite3_vtab 객체의 pModule, nRef, zErrMsg 필드를 초기화할 필요가 없어요. SQLite 코어가 그 일을 처리할 거예요.

xCreate는 새 가상 테이블을 만드는 데 성공하면 SQLITE_OK를, 성공하지 못하면 SQLITE_ERROR를 반환해야 해요. 성공하지 못하면 sqlite3_vtab 구조체를 할당해서는 안 돼요. 실패하면 *pzErr에 오류 메시지를 선택적으로 반환할 수 있어요. 오류 메시지 문자열을 담을 공간은 sqlite3_malloc()이나 sqlite3_mprintf() 같은 SQLite 메모리 할당 함수로 할당해야 해요. 오류가 응용 프로그램에 보고된 후 SQLite 코어가 sqlite3_free()로 공간을 해제하려고 시도할 것이기 때문이에요.

xCreate 메서드가 생략되면(NULL 포인터로 남겨지면) 가상 테이블은 동명 전용 가상 테이블이에요. CREATE VIRTUAL TABLE로 새 가상 테이블 인스턴스를 만들 수 없고 가상 테이블은 모듈 이름으로만 사용할 수 있어요. 3.9.0(2015-10-14) 이전 SQLite 버전은 동명 전용 가상 테이블을 이해하지 못하며, xCreate 메서드가 null인지 확인하지 않기 때문에 동명 전용 가상 테이블에서 CREATE VIRTUAL TABLE을 시도하면 세그폴트(segfault)가 발생한다는 점에 주의하세요.

xCreate 메서드가 xConnect 메서드와 정확히 같은 포인터라면 가상 테이블이 백킹 저장소(backing store)를 초기화할 필요가 없음을 나타내요. 그런 가상 테이블은 동명 가상 테이블 또는 CREATE VIRTUAL TABLE을 사용한 명명된 가상 테이블 또는 둘 다로 사용될 수 있어요.

2.1.1. 가상 테이블의 숨은 열 (Hidden columns in virtual tables)

열 데이터 타입이 특수 키워드 "HIDDEN"(대소문자 조합)을 포함하면 그 키워드는 열 데이터 타입 이름에서 제외되고 열은 내부적으로 숨은 열(hidden column)로 표시돼요. 숨은 열은 세 가지 측면에서 일반 열과 달라요:

  • 숨은 열은 "PRAGMA table_info"가 반환하는 데이터 집합에 나열되지 않아요.
  • 숨은 열은 SELECT의 결과 집합에서 "*" 표현식의 확장에 포함되지 않아요.
  • 숨은 열은 명시적 열 목록이 없는 INSERT 문이 사용하는 암시적 열 목록에 포함되지 않아요.

예를 들어 다음 SQL이 sqlite3_declare_vtab()에 전달되면:

CREATE TABLE x(a HIDDEN VARCHAR(12), b INTEGER, c INTEGER Hidden);

가상 테이블은 두 개의 숨은 열과 "VARCHAR(12)" 및 "INTEGER" 데이터 타입으로 생성될 거예요.

숨은 열의 사용 예는 FTS3 가상 테이블 구현에서 볼 수 있어요. 모든 FTS 가상 테이블에는 가상 테이블에서 FTS 보조 함수FTS MATCH 연산자로 정보를 전달하는 데 사용되는 FTS 숨은 열이 있어요.

2.1.2. 테이블 값 함수 (Table-valued functions)

숨은 열을 포함하는 가상 테이블SELECT 문의 FROM 절에서 테이블 값 함수처럼 사용될 수 있어요. 테이블 값 함수의 인자는 가상 테이블의 HIDDEN 열에 대한 제약이 돼요.

예를 들어 "generate_series" 확장(소스 트리ext/misc/series.c 파일에 있음)은 다음 스키마를 가진 동명 가상 테이블을 구현해요:

CREATE TABLE generate_series(
  value,
  start HIDDEN,
  stop HIDDEN,
  step HIDDEN
);

이 테이블 구현의 sqlite3_module.xBestIndex 메서드는 HIDDEN 열에 대한 동등성 제약을 확인하고 그것을 입력 매개변수로 사용해 생성할 정수 "value" 출력의 범위를 결정해요. 제약되지 않은 열에는 합리적인 기본값이 사용돼요. 예를 들어 5와 50 사이의 모든 정수를 나열하려면:

SELECT value FROM generate_series(5,50);

이전 쿼리는 다음 것과 동등해요:

SELECT value FROM generate_series WHERE start=5 AND stop=50;

가상 테이블 이름의 인자는 숨은 열에 순서대로 대응돼요. 인자 수는 숨은 열 수보다 적을 수 있는데, 그 경우 뒤쪽 숨은 열은 제약되지 않아요. 하지만 인자가 가상 테이블의 숨은 열 수보다 많으면 오류가 발생해요.

2.1.3. WITHOUT ROWID 가상 테이블 (WITHOUT ROWID Virtual Tables)

SQLite 3.14.0 버전(2016-08-08)부터 sqlite3_declare_vtab()에 전달되는 CREATE TABLE 문은 WITHOUT ROWID 절을 포함할 수 있어요. 이것은 가상 테이블 행을 고유 정수에 쉽게 매핑할 수 없는 경우에 유용해요. WITHOUT ROWID를 포함하는 CREATE TABLE 문은 하나 이상의 열을 PRIMARY KEY로 정의해야 해요. PRIMARY KEY의 각 열은 개별적으로 NOT NULL이어야 하고 각 행의 모든 열은 집합적으로 고유해야 해요.

SQLite가 WITHOUT ROWID 가상 테이블에 대해 PRIMARY KEY를 강제하지 않는다는 점에 주의하세요. 강제는 기본 가상 테이블 구현의 책임이에요. 하지만 SQLite는 PRIMARY KEY 제약이 유효하다고(식별된 열이 정말 UNIQUE이고 NOT NULL이라고) 가정하고, 그 가정을 사용해 가상 테이블에 대한 쿼리를 최적화해요.

WITHOUT ROWID 가상 테이블에서는 (물론) rowid 열에 접근할 수 없어요.

xUpdate 메서드는 원래 ROWID를 단일 값으로 갖도록 설계되었어요. xUpdate 메서드는 ROWID 대신 임의의 PRIMARY KEY를 수용하도록 확장되었지만, PRIMARY KEY는 여전히 한 열뿐이어야 해요. 이런 이유로 SQLite는 PRIMARY KEY 열이 둘 이상이고 xUpdate 메서드가 NULL이 아닌 WITHOUT ROWID 가상 테이블을 거부할 거예요.

2.2. xConnect 메서드

int (*xConnect)(sqlite3*, void *pAux,
             int argc, char *const*argv,
             sqlite3_vtab **ppVTab,
             char **pzErr);

xConnect 메서드는 xCreate와 매우 비슷해요. 같은 매개변수를 가지며 xCreate처럼 새 sqlite3_vtab 구조체를 구성해요. 그리고 xCreate처럼 sqlite3_declare_vtab()을 호출해야 해요. xCreate가 하는 모든 sqlite3_vtab_config() 호출도 해야 해요.

차이는 xConnect가 기존 가상 테이블에 대한 새 연결을 설정하기 위해 호출되는 반면 xCreate는 처음부터 새 가상 테이블을 만들기 위해 호출된다는 것이에요.

xCreate와 xConnect 메서드는 가상 테이블이 처음 생성될 때 초기화해야 하는 일종의 백킹 저장소를 가질 때만 달라요. xCreate 메서드는 백킹 저장소를 만들고 초기화해요. xConnect 메서드는 기존 백킹 저장소에 연결만 해요. xCreate와 xConnect가 같으면 테이블은 동명 가상 테이블이에요.

예를 들어 디스크의 기존 CSV(comma-separated-value) 파일에 읽기 전용 접근을 제공하는 가상 테이블 구현을 고려해요. 그러한 가상 테이블에는 만들거나 초기화할 백킹 저장소가 없으므로(CSV 파일이 이미 디스크에 있으므로) 그 모듈의 xCreate와 xConnect 메서드는 동일할 거예요.

또 다른 예는 전문 색인(full-text index)을 구현하는 가상 테이블이에요. xCreate 메서드는 그 색인의 사전과 포스팅 목록(posting list)을 담을 데이터 구조를 만들고 초기화해야 해요. 반면 xConnect 메서드는 이전 xCreate 호출이 만든 기존 사전과 포스팅 목록을 찾아 사용하기만 하면 돼요.

xConnect 메서드는 새 가상 테이블을 만드는 데 성공하면 SQLITE_OK를, 성공하지 못하면 SQLITE_ERROR를 반환해야 해요. 성공하지 못하면 sqlite3_vtab 구조체를 할당해서는 안 돼요. 실패하면 *pzErr에 오류 메시지를 선택적으로 반환할 수 있어요. 오류 메시지 문자열을 담을 공간은 sqlite3_malloc()이나 sqlite3_mprintf() 같은 SQLite 메모리 할당 함수로 할당해야 해요. 오류가 응용 프로그램에 보고된 후 SQLite 코어가 sqlite3_free()로 공간을 해제하려고 시도할 것이기 때문이에요.

가상 테이블이 백킹 저장소를 초기화할 필요가 없으면 sqlite3_module 객체의 xCreate와 xConnect 포인터가 같은 함수를 가리킬 수 있지만, xConnect 메서드는 모든 가상 테이블 구현에 필요해요.

2.3. xBestIndex 메서드

SQLite는 가상 테이블 모듈의 xBestIndex 메서드를 사용해 가상 테이블에 접근하는 최선의 방법을 결정해요. xBestIndex 메서드의 프로토타입은 다음과 같아요:

int (*xBestIndex)(sqlite3_vtab *pVTab, sqlite3_index_info*);

SQLite 코어는 sqlite3_index_info 구조체의 특정 필드를 채우고 그 구조체에 대한 포인터를 두 번째 매개변수로 xBestIndex에 전달함으로써 xBestIndex 메서드와 통신해요. xBestIndex 메서드는 답변을 형성하는 이 구조체의 다른 필드를 채워요. sqlite3_index_info 구조체는 다음과 같아요:

struct sqlite3_index_info {
  /* Inputs */
  const int nConstraint;     /* Number of entries in aConstraint */
  const struct sqlite3_index_constraint {
     int iColumn;              /* Column constrained.  -1 for ROWID */
     unsigned char op;         /* Constraint operator */
     unsigned char usable;     /* True if this constraint is usable */
     int iTermOffset;          /* Used internally - xBestIndex should ignore */
  } *const aConstraint;      /* Table of WHERE clause constraints */
  const int nOrderBy;        /* Number of terms in the ORDER BY clause */
  const struct sqlite3_index_orderby {
     int iColumn;              /* Column number */
     unsigned char desc;       /* True for DESC.  False for ASC. */
  } *const aOrderBy;         /* The ORDER BY clause */

  /* Outputs */
  struct sqlite3_index_constraint_usage {
    int argvIndex;           /* if >0, constraint is part of argv to xFilter */
    unsigned char omit;      /* Do not code a test for this constraint */
  } *const aConstraintUsage;
  int idxNum;                /* Number used to identify the index */
  char *idxStr;              /* String, possibly obtained from sqlite3_malloc */
  int needToFreeIdxStr;      /* Free idxStr using sqlite3_free() if true */
  int orderByConsumed;       /* True if output is already ordered */
  double estimatedCost;      /* Estimated cost of using this index */
  /* Fields below are only available in SQLite 3.8.2 and later */
  sqlite3_int64 estimatedRows;    /* Estimated number of rows returned */
  /* Fields below are only available in SQLite 3.9.0 and later */
  int idxFlags;              /* Mask of SQLITE_INDEX_SCAN_* flags */
  /* Fields below are only available in SQLite 3.10.0 and later */
  sqlite3_uint64 colUsed;    /* Input: Mask of columns used by statement */
};

"estimatedRows", "idxFlags", "colUsed" 필드에 대한 경고에 주의하세요. 이 필드는 각각 SQLite 버전 3.8.2, 3.9.0, 3.10.0에 추가되었어요. 이 필드를 읽거나 쓰는 모든 확장은 먼저 사용 중인 SQLite 라이브러리의 버전이 적절한 버전 이상인지 확인해야 해요. 아마 sqlite3_libversion_number()가 반환한 값을 3008002, 3009000 및/또는 3010000 상수와 비교하는 방식으로 확인할 거예요. 이전 버전의 SQLite가 만든 sqlite3_index_info 구조체에서 이 필드에 접근하려는 시도의 결과는 정의되지 않아요.

게다가 정의된 상수들이 있어요:

#define SQLITE_INDEX_CONSTRAINT_EQ         2
#define SQLITE_INDEX_CONSTRAINT_GT         4
#define SQLITE_INDEX_CONSTRAINT_LE         8
#define SQLITE_INDEX_CONSTRAINT_LT        16
#define SQLITE_INDEX_CONSTRAINT_GE        32
#define SQLITE_INDEX_CONSTRAINT_MATCH     64
#define SQLITE_INDEX_CONSTRAINT_LIKE      65  /* 3.10.0 and later */
#define SQLITE_INDEX_CONSTRAINT_GLOB      66  /* 3.10.0 and later */
#define SQLITE_INDEX_CONSTRAINT_REGEXP    67  /* 3.10.0 and later */
#define SQLITE_INDEX_CONSTRAINT_NE        68  /* 3.21.0 and later */
#define SQLITE_INDEX_CONSTRAINT_ISNOT     69  /* 3.21.0 and later */
#define SQLITE_INDEX_CONSTRAINT_ISNOTNULL 70  /* 3.21.0 and later */
#define SQLITE_INDEX_CONSTRAINT_ISNULL    71  /* 3.21.0 and later */
#define SQLITE_INDEX_CONSTRAINT_IS        72  /* 3.21.0 and later */
#define SQLITE_INDEX_CONSTRAINT_LIMIT     73  /* 3.38.0 and later */
#define SQLITE_INDEX_CONSTRAINT_OFFSET    74  /* 3.38.0 and later */
#define SQLITE_INDEX_CONSTRAINT_FUNCTION 150  /* 3.25.0 and later */
#define SQLITE_INDEX_SCAN_UNIQUE           1  /* Scan visits at most 1 row */

i번째 제약을 평가할 때 사용해야 할 정렬 순서(콜레이션)의 이름을 찾으려면 sqlite3_vtab_collation() 인터페이스를 사용해요:

const char *sqlite3_vtab_collation(sqlite3_index_info*, int i);

SQLite 코어는 가상 테이블을 포함하는 쿼리를 컴파일할 때 xBestIndex 메서드를 호출해요. 즉 SQLite는 sqlite3_prepare() 또는 그에 상응하는 것을 실행할 때 이 메서드를 호출해요. 이 메서드를 호출함으로써 SQLite 코어는 가상 테이블의 일부 행 부분 집합에 접근해야 하며 그 접근을 수행하는 가장 효율적인 방법을 알고 싶다고 가상 테이블에 말하는 것이에요. xBestIndex 메서드는 SQLite 코어가 이후 사용해 가상 테이블의 효율적인 검색을 수행할 수 있는 정보로 답해요.

단일 SQL 쿼리를 컴파일하는 동안 SQLite 코어는 sqlite3_index_info의 다른 설정으로 xBestIndex를 여러 번 호출할 수 있어요. 그런 다음 SQLite 코어는 가장 좋은 성능을 줄 것으로 보이는 조합을 선택할 거예요.

이 메서드를 호출하기 전에 SQLite 코어는 현재 처리하려는 쿼리에 대한 정보로 sqlite3_index_info 구조체 인스턴스를 초기화해요. 이 정보는 주로 쿼리의 WHERE 절과 ORDER BY 또는 GROUP BY 절에서, 그리고 쿼리가 조인이라면 ON 또는 USING 절에서도 파생돼요. SQLite 코어가 xBestIndex 메서드에 제공하는 정보는 구조체에서 "Inputs"로 표시된 부분에 담겨요. "Outputs" 부분은 0으로 초기화돼요.

sqlite3_index_info 구조체의 정보는 일시적이며 xBestIndex 메서드가 반환하는 즉시 덮어쓰거나 할당 해제될 수 있어요. xBestIndex 메서드가 sqlite3_index_info 구조체의 어떤 부분을 기억해야 한다면 사본을 만들어야 해요. 사본을 할당 해제될 곳, 예를 들어 needToFreeIdxStr을 1로 설정한 idxStr 필드에 저장하도록 주의해야 해요.

xBestIndex가 언제나 xFilter 전에 호출될 것이라는 점에 주의하세요. xBestIndex의 idxNum과 idxStr 출력이 xFilter의 필수 입력이기 때문이에요. 하지만 성공적인 xBestIndex 다음에 xFilter가 호출될 것이라는 보장은 없어요.

xBestIndex 메서드는 모든 가상 테이블 구현에 필요해요.

2.3.1. 입력 (Inputs)

SQLite 코어가 가상 테이블에 전달하려는 주된 것은 검색할 행 수를 제한하는 데 사용할 수 있는 제약이에요. aConstraint[] 배열은 각 제약에 대해 하나의 항목을 담아요. 그 배열에는 정확히 nConstraint개의 항목이 있을 거예요.

각 제약은 보통 다음 형식의 WHERE 절 또는 USING이나 ON 절의 항에 대응해요:

column OP EXPR

여기서 "column"은 가상 테이블의 열이고, OP는 "=" 또는 "<" 같은 연산자이며, EXPR은 임의의 표현식이에요. 예를 들어 WHERE 절에 다음과 같은 항이 있으면:

a = 5

제약 중 하나는 연산자 "="와 표현식 "5"인 "a" 열에 대한 것이에요. 제약이 WHERE 절의 리터럴 표현을 가질 필요는 없어요. 쿼리 최적화기는 최대한 많은 제약을 추출하기 위해 WHERE 절을 변형할 수 있어요. 예를 들어 WHERE 절에 다음과 같은 것이 있으면:

x BETWEEN 10 AND 100 AND 999>y

쿼리 최적화기는 이것을 세 개의 별도 제약으로 변환할 수 있어요:

x >= 10
x <= 100
y < 999

각 제약에 대해 aConstraint[].iColumn 필드는 어떤 열이 제약의 왼쪽에 나타나는지 나타내요. 가상 테이블의 첫 번째 열은 열 0이에요. 가상 테이블의 rowid는 열 -1이에요. aConstraint[].op 필드는 어떤 연산자가 사용되는지 나타내요. SQLITE_INDEX_CONSTRAINT_* 상수는 정수 상수를 연산자 값에 매핑해요. 열은 xCreate 또는 xConnect 메서드에서 sqlite3_declare_vtab() 호출이 정의한 순서대로 나타나요. 숨은 열도 열 인덱스를 결정할 때 세어요.

가상 테이블의 xFindFunction() 메서드가 정의되고, xFindFunction()이 때때로 SQLITE_INDEX_CONSTRAINT_FUNCTION 또는 그보다 큰 값을 반환하면, 제약은 다음 형식일 수도 있어요:

FUNCTION( column, EXPR)

이 경우 aConstraint[].op 값은 FUNCTION에 대해 xFindFunction()이 반환한 값과 같아요.

aConstraint[] 배열은 가상 테이블에 적용되는 모든 제약에 대한 정보를 담아요. 하지만 조인에서 테이블이 정렬되는 방식 때문에 일부 제약은 사용할 수 없을 수도 있어요. 따라서 xBestIndex 메서드는 aConstraint[].usable 플래그가 참인 제약만 고려해야 해요.

WHERE 절 제약에 더해 SQLite 코어는 xBestIndex 메서드에 ORDER BY 절에 대해서도 알려줘요. (집계 쿼리에서 SQLite 코어는 ORDER BY 절 정보 대신 GROUP BY 절 정보를 넣을 수 있지만, 이 사실은 xBestIndex 메서드에 어떤 차이를 만들지 않아야 해요.) ORDER BY 절의 모든 항이 가상 테이블의 열이면 nOrderBy는 ORDER BY 절의 항 수이고 aOrderBy[] 배열은 order by 절의 각 항에 대한 열과 그 열이 ASC인지 DESC인지를 식별해요.

SQLite 3.10.0 버전(2016-01-06) 이상에서 colUsed 필드를 사용해 준비되는 문이 실제로 사용하는 가상 테이블의 필드가 무엇인지 나타낼 수 있어요. colUsed의 최하위 비트가 설정되면 첫 번째 열이 사용된다는 뜻이에요. 두 번째 최하위 비트가 두 번째 열에 대응해요. 이런 식으로 이어져요. colUsed의 최상위 비트가 설정되면 처음 63개 열 이외의 열이 하나 이상 사용된다는 뜻이에요. 열 사용 정보가 xFilter 메서드에 필요하면 필요한 비트를 출력 idxNum 필드나 idxStr 내용에 인코딩해야 해요.

2.3.1.1. LIKE, GLOB, REGEXP, MATCH 함수

LIKE, GLOB, REGEXP, MATCH 연산자에 대해 aConstraint[].iColumn 값은 연산자의 왼쪽 피연산자인 가상 테이블 열이에요. 하지만 이 연산자가 연산자 대신 함수 호출로 표현되면 aConstraint[].iColumn 값은 그 함수의 두 번째 인자인 가상 테이블 열을 참조해요:

LIKE(EXPR, column) GLOB(EXPR, column) REGEXP(EXPR, column) MATCH(EXPR, column)

따라서 xBestIndex() 메서드가 보기에 다음 두 형식은 동등해요:

column LIKE EXPR LIKE(EXPR,column)

함수의 두 번째 인자를 보는 이 특별한 동작은 LIKE, GLOB, REGEXP, MATCH 함수에만 발생해요. 다른 모든 함수에 대해 aConstraint[].iColumn 값은 함수의 첫 번째 인자를 참조해요.

하지만 LIKE, GLOB, REGEXP, MATCH의 이 특수 기능은 xFindFunction() 메서드에는 적용되지 않아요. xFindFunction() 메서드는 항상 LIKE, GLOB, REGEXP 또는 MATCH 연산자의 왼쪽 피연산자에 기반하지만, 그 연산자의 함수 호출 동등 형태에서는 첫 번째 인자에 기반해요.

2.3.1.2. LIMIT와 OFFSET

aConstraint[].op가 SQLITE_INDEX_CONSTRAINT_LIMIT 또는 SQLITE_INDEX_CONSTRAINT_OFFSET 중 하나이면, 가상 테이블을 사용하는 SQL 쿼리 문에 LIMIT 또는 OFFSET 절이 있음을 나타내요. LIMIT와 OFFSET 연산자는 왼쪽 피연산자가 없으므로, aConstraint[].op가 SQLITE_INDEX_CONSTRAINT_LIMIT 또는 SQLITE_INDEX_CONSTRAINT_OFFSET 중 하나이면 aConstraint[].iColumn 값은 무의미하고 사용해서는 안 돼요.

2.3.1.3. 제약의 오른쪽 값 (Right-hand side values of constraints)

sqlite3_vtab_rhs_value() 인터페이스를 사용해 제약의 오른쪽 피연산자에 접근하려 시도할 수 있어요. 하지만 오른쪽 연산자의 값은 xBestIndex 메서드가 실행되는 시점에 알려지지 않을 수 있어서 sqlite3_vtab_rhs_value() 호출이 성공하지 못할 수 있어요. 보통 제약의 오른쪽 피연산자는 입력 SQL에서 리터럴 값으로 코딩된 경우에만 xBestIndex에 사용 가능해요. 오른쪽 피연산자가 표현식이나 호스트 매개변수로 코딩되면 아마 xBestIndex에 접근할 수 없을 거예요. SQLITE_INDEX_CONSTRAINT_ISNULLSQLITE_INDEX_CONSTRAINT_ISNOTNULL 같은 일부 연산자는 오른쪽 피연산자가 없어요. sqlite3_vtab_rhs_value() 인터페이스는 그런 연산자에 대해 항상 SQLITE_NOTFOUND를 반환해요.

2.3.2. 출력 (Outputs)

위의 모든 정보가 주어졌을 때 xBestIndex 메서드의 역할은 가상 테이블을 검색하는 최선의 방법을 알아내는 것이에요.

xBestIndex 메서드는 idxNum과 idxStr 필드를 통해 xFilter 메서드에 색인 전략을 전달해요. idxNum 값과 idxStr 문자열 내용은 SQLite 코어가 보기에 임의적이며, xBestIndex와 xFilter가 그 의미에 동의하는 한 어떤 의미든 가질 수 있어요. SQLite 코어는 idxStr이 참조하는 문자 시퀀스가 NUL로 끝난다고만 가정하고 xBestIndex의 정보를 xFilter 메서드로 그냥 복사해요.

idxStr 값은 sqlite3_mprintf() 같은 SQLite 메모리 할당 함수에서 얻은 문자열일 수 있어요. 그 경우 needToFreeIdxStr 플래그를 참으로 설정해 SQLite 코어가 그 문자열을 사용한 후 sqlite3_free()를 호출해 메모리 누수를 피하도록 해야 해요. idxStr 값은 정적 상수 문자열일 수도 있는데, 그 경우 needToFreeIdxStr 불리언은 false로 남아 있어야 해요.

estimatedCost 필드는 가상 테이블에 대해 이 쿼리를 실행하는 데 필요한 디스크 접근 연산의 추정 수로 설정해야 해요. SQLite 코어는 종종 다른 제약으로 xBestIndex를 여러 번 호출하고 여러 비용 추정치를 얻은 다음 가장 낮은 추정치를 주는 쿼리 계획을 선택해요. SQLite 코어는 xBestIndex를 호출하기 전에 estimatedCost를 아주 큰 값으로 초기화하므로, xBestIndex가 현재 매개변수 조합이 바람직하지 않다고 판단하면 estimatedCost 필드를 변경하지 않은 채로 두어 그 사용을 단념시키는(경고) 방식으로 할 수 있어요.

현재 SQLite 버전이 3.8.2 이상이면 estimatedRows 필드를 제안된 쿼리 계획이 반환하는 행 수의 추정치로 설정할 수 있어요. 이 값이 명시적으로 설정되지 않으면 기본 추정치 25행이 사용돼요.

현재 SQLite 버전이 3.9.0 이상이면 idxFlags 필드를 SQLITE_INDEX_SCAN_UNIQUE로 설정해 가상 테이블이 주어진 입력 제약으로 0행 또는 1행만 반환할 것임을 나타낼 수 있어요. idxFlags 필드의 추가 비트는 이후 SQLite 버전에서 이해될 수 있어요.

aConstraintUsage[] 배열은 sqlite3_index_info 구조체 입력 부분의 nConstraint 제약 각각에 대해 하나의 요소를 담아요. xBestIndex가 제약을 어떻게 사용하는지 코어에 알리기 위해 aConstraintUsage[] 배열을 사용해요.

xBestIndex 메서드는 aConstraintUsage[].argvIndex 항목을 0보다 큰 값으로 설정할 수 있어요. 정확히 하나의 항목이 1로, 다른 하나가 2로, 또 다른 하나가 3으로 등 xBestIndex 메서드가 원하는 만큼 또는 그만큼 설정되어야 해요. 해당 제약의 EXPR이 그다음 xFilter의 argv[] 매개변수로 전달될 거예요.

예를 들어 aConstraint[3].argvIndex가 1로 설정되면 xFilter가 호출될 때 xFilter에 전달되는 argv[0]이 aConstraint[3] 제약의 EXPR 값을 가질 거예요.

2.3.2.1. 바이트코드에서 제약 확인 생략 (Omit constraint checking in bytecode)

기본적으로 SQLite는 가상 테이블의 각 행에 대한 모든 제약을 이중 확인(double-check)해 충족되는지 검증하는 바이트코드를 생성해요. 가상 테이블이 제약이 항상 충족될 것임을 보장할 수 있으면 aConstraintUsage[].omit를 설정해 그 이중 확인을 억제하려 시도할 수 있어요. 하지만 몇 가지 예외를 제외하고 이것은 힌트일 뿐이며 중복 제약 검사가 억제된다는 보장은 없어요. 핵심 요점:

  • omit 플래그는 제약의 argvIndex 값이 0보다 크고 16보다 작거나 같을 때만 존중돼요. 오른쪽 피연산자를 xFilter 메서드에 전달하지 않는 제약에 대해서는 제약 검사가 결코 억제되지 않아요. 현재 구현은 xFilter에 전달된 처음 16개 값에 대해서만 중복 제약 검사를 억제할 수 있지만, 그 제한은 향후 릴리스에서 증가될 수 있어요.

  • omit 플래그는 argvIndex가 0보다 큰 한 SQLITE_INDEX_CONSTRAINT_OFFSET 제약에 대해 항상 존중돼요. SQLITE_INDEX_CONSTRAINT_OFFSET 제약에 omit 플래그를 설정하는 것은 가상 테이블이 처음 N행 출력을 스스로 억제할 것임을 SQLite에 나타내는 것이에요. 여기서 N은 OFFSET 연산자의 오른쪽 피연산자예요. 가상 테이블 구현이 SQLITE_INDEX_CONSTRAINT_OFFSET 제약에 omit를 설정했지만 처음 N행 출력을 억제하지 못하면 전체 쿼리의 결과가 잘못돼요.

2.3.2.2. ORDER BY와 orderByConsumed

가상 테이블이 ORDER BY 절이 지정한 순서로 행을 출력할 것이라면 orderByConsumed 플래그를 true로 설정할 수 있어요. 출력이 자동으로 올바른 순서가 아니라면 orderByConsumed는 기본값 false로 남겨야 해요. 이것은 SQLite 코어에 가상 테이블에서 나온 데이터에 별도의 정렬 패스가 필요하다고 나타낼 거예요. orderByConsumed를 설정하는 것은 최적화예요. orderByConsumed를 기본값(0)으로 두면 쿼리는 항상 올바른 답을 얻을 거예요. orderByConsumed를 설정하면 불필요한 정렬 연산을 피해 쿼리가 더 빨라질 수 있지만, orderByConsumed를 잘못 설정하면 잘못된 답이 나올 수 있어요. 새 가상 테이블 구현은 처음에는 orderByConsumed 값을 설정하지 않은 채 두고, 다른 모든 것이 올바르게 동작한다는 것을 안 다음에 돌아가 적절한 곳에 orderByConsumed를 설정해 최적화를 시도하는 것이 좋아요.

때로는 가상 테이블의 출력이 nOrderBy와 aOrderBy가 지정한 순서와 엄밀히 일치하지 않아도 orderByConsumed 플래그를 안전하게 설정할 수 있어요. sqlite3_vtab_distinct() 인터페이스가 1 또는 2를 반환하면 순서를 완화할 수 있음을 나타내요. 자세한 정보는 sqlite3_vtab_distinct() 문서를 참고하세요.

2.3.3. 반환 값 (Return Value)

xBestIndex 메서드는 성공 시 SQLITE_OK를 반환해야 해요. 어떤 종류의 치명적 오류가 발생하면 적절한 오류 코드(예: SQLITE_NOMEM)를 대신 반환해야 해요.

xBestIndex가 SQLITE_CONSTRAINT를 반환하면 그것은 오류를 나타내지 않아요. 오히려 SQLITE_CONSTRAINT는 지정된 입력 매개변수의 특정 조합이 가상 테이블이 자신의 일을 하기에 불충분함을 나타내요. 이것은 논리적으로 estimatedCost를 무한대로 설정하는 것과 같아요. 특정 쿼리 계획에 대한 xBestIndex의 모든 호출이 SQLITE_CONSTRAINT를 반환하면 가상 테이블을 안전하게 사용할 방법이 없다는 뜻이고, sqlite3_prepare() 호출이 "no query solution" 오류로 실패할 거예요.

2.3.4. 테이블 값 함수에서 필수 매개변수 강제하기 (Enforcing Required Parameters On Table-Valued Functions)

xBestIndex의 SQLITE_CONSTRAINT 반환은 필수 매개변수가 있는 테이블 값 함수에 유용해요. 필수 매개변수 중 하나에 대해 aConstraint[].usable 필드가 false이면 xBestIndex 메서드는 SQLITE_CONSTRAINT를 반환해야 해요. 필수 필드가 aConstraint[] 배열에 전혀 나타나지 않으면 해당 매개변수가 입력 SQL에서 생략되었음을 의미해요. 그 경우 xBestIndex는 pVTab->zErrMsg에 오류 메시지를 설정하고 SQLITE_ERROR를 반환해야 해요. 요약하면:

  1. 필수 매개변수에 대한 aConstraint[].usable 값이 false → SQLITE_CONSTRAINT를 반환한다.
  2. 필수 매개변수가 aConstraint[] 배열 어디에도 나타나지 않음 → pVTab->zErrMsg에 오류 메시지를 설정하고 SQLITE_ERROR를 반환한다.

다음 예제가 xBestIndex의 반환 값으로서 SQLITE_CONSTRAINT의 사용을 더 잘 설명할 거예요:

SELECT * FROM realtab, tablevaluedfunc(realtab.x);

"tablevaluedfunc"의 첫 번째 숨은 열이 "param1"이라고 가정하면, 위 쿼리는 의미상 다음 것과 동등해요:

SELECT * FROM realtab, tablevaluedfunc
 WHERE tablevaluedfunc.param1 = realtab.x;

쿼리 계획자는 이 쿼리의 많은 가능한 구현 사이에서 결정해야 하지만, 특히 두 계획이 주목할 만해요:

  1. realtab의 모든 행을 스캔하고 각 행에 대해 param1이 realtab.x와 같은 tablevaluedfunc의 행을 찾는다.
  2. tablevaluedfunc의 모든 행을 스캔하고 각 행에 대해 x가 tablevaluedfunc.param1과 같은 realtab의 행을 찾는다.

xBestIndex 메서드는 위 잠재적 계획 각각에 대해 한 번씩 호출될 거예요. 계획 1의 경우 "param1 = ?" 제약의 오른쪽 값이 바깥쪽 realtab 루프에 의해 결정되므로 알려져 있기 때문에 param1 열의 SQLITE_CONSTRAINT_EQ 제약에 대한 aConstraint[].usable 플래그가 true가 될 거예요. 하지만 계획 2의 경우 "param1 = ?"의 오른쪽 값이 안쪽 루프에 의해 결정되어 미지수이므로 aConstraint[].usable 플래그가 false가 될 거예요. param1이 테이블 값 함수의 필수 입력이므로 xBestIndex 메서드는 계획 2가 제시될 때 SQLITE_CONSTRAINT를 반환해 필수 입력이 없음을 나타내야 해요. 이것은 쿼리 계획자가 계획 1을 선택하도록 강제해요.

2.4. xDisconnect 메서드

int (*xDisconnect)(sqlite3_vtab *pVTab);

이 메서드는 가상 테이블에 대한 연결을 해제해요. sqlite3_vtab 객체만 파괴돼요. 가상 테이블은 파괴되지 않고 가상 테이블과 관련된 백킹 저장소는 지속돼요. 이 메서드는 xConnect의 작업을 되돌려요.

이 메서드는 가상 테이블에 대한 연결의 소멸자예요. 이 메서드를 xDestroy와 대조하세요. xDestroy는 전체 가상 테이블의 소멸자예요.

xDisconnect 메서드는 모든 가상 테이블 구현에 필요하지만, 특정 가상 테이블에 대해 합리적이라면 xDisconnect와 xDestroy 메서드가 같은 함수여도 괜찮아요.

2.5. xDestroy 메서드

int (*xDestroy)(sqlite3_vtab *pVTab);

이 메서드는 xDisconnect 메서드처럼 가상 테이블에 대한 연결을 해제하고, 또한 기본 테이블 구현을 파괴해요. 이 메서드는 xCreate의 작업을 되돌려요.

xDisconnect 메서드는 가상 테이블을 사용하는 데이터베이스 연결이 닫힐 때마다 호출돼요. xDestroy 메서드는 가상 테이블에 대해 DROP TABLE 문이 실행될 때만 호출돼요.

xDestroy 메서드는 모든 가상 테이블 구현에 필요하지만, 특정 가상 테이블에 대해 합리적이라면 xDisconnect와 xDestroy 메서드가 같은 함수여도 괜찮아요.

2.6. xOpen 메서드

int (*xOpen)(sqlite3_vtab *pVTab, sqlite3_vtab_cursor **ppCursor);

xOpen 메서드는 가상 테이블에 접근(읽기 및/또는 쓰기)하는 데 사용되는 새 커서를 만들어요. 이 메서드의 성공적인 호출은 sqlite3_vtab_cursor(또는 하위 클래스)용 메모리를 할당하고, 새 객체를 초기화하며, *ppCursor가 새 객체를 가리키게 해요. 그런 다음 성공적인 호출은 SQLITE_OK를 반환해요.

이 메서드에 대한 성공적인 호출마다 SQLite 코어는 나중에 xClose 메서드를 호출해 할당된 커서를 파괴할 거예요.

xOpen 메서드는 sqlite3_vtab_cursor 구조체의 pVtab 필드를 초기화할 필요가 없어요. SQLite 코어가 그 일을 자동으로 처리할 거예요.

가상 테이블 구현은 임의의 수의 동시에 열린 커서를 지원할 수 있어야 해요.

처음 열릴 때 커서는 정의되지 않은 상태예요. SQLite 코어는 커서를 위치시키거나 읽으려 시도하기 전에 커서에 xFilter 메서드를 호출할 거예요.

xOpen 메서드는 모든 가상 테이블 구현에 필요해요.

2.7. xClose 메서드

int (*xClose)(sqlite3_vtab_cursor*);

xClose 메서드는 xOpen이 이전에 연 커서를 닫아요. SQLite 코어는 항상 xOpen으로 열린 각 커서에 대해 xClose를 한 번 호출할 거예요.

이 메서드는 해당 xOpen 호출이 할당한 모든 자원을 해제해야 해요. 이 루틴은 오류를 반환해도 다시 호출되지 않을 거예요. SQLite 코어는 sqlite3_vtab_cursor가 닫힌 후에는 다시 사용하지 않을 거예요.

xClose 메서드는 모든 가상 테이블 구현에 필요해요.

2.8. xEof 메서드

int (*xEof)(sqlite3_vtab_cursor*);

xEof 메서드는 지정된 커서가 현재 유효한 데이터 행을 가리키면 false(0)를, 그렇지 않으면 true(0이 아닌 값)를 반환해야 해요. 이 메서드는 SQL 엔진이 각 xFilterxNext 호출 직후에 호출해요.

xEof 메서드는 모든 가상 테이블 구현에 필요해요.

2.9. xFilter 메서드

int (*xFilter)(sqlite3_vtab_cursor*, int idxNum, const char *idxStr,
              int argc, sqlite3_value **argv);

이 메서드는 가상 테이블 검색을 시작해요. 첫 번째 인자는 xOpen이 연 커서예요. 다음 두 인자는 xBestIndex가 이전에 선택한 특정 검색 색인을 정의해요. idxNum과 idxStr의 구체적인 의미는 xFilter와 xBestIndex가 그 의미에 동의하는 한 중요하지 않아요.

xBestIndex 함수는 sqlite3_index_info 구조체의 aConstraintUsage[].argvIndex 값을 사용해 특정 표현식의 값을 요청했을 수 있어요. 그 값은 argc와 argv 매개변수를 사용해 xFilter에 전달돼요.

가상 테이블에 검색 기준과 일치하는 행이 하나 이상 있으면 커서를 첫 번째 행을 가리키도록 남겨야 해요. 이후 xEof 호출은 false(0)를 반환해야 해요. 일치하는 행이 없으면 커서를 xEof가 true(0이 아닌 값)를 반환하게 하는 상태로 남겨야 해요. SQLite 엔진은 xColumnxRowid 메서드를 사용해 그 행 내용에 접근할 거예요. xNext 메서드가 다음 행으로 진행하는 데 사용될 거예요.

이 메서드는 성공하면 SQLITE_OK를, 오류가 발생하면 sqlite 오류 코드를 반환해야 해요.

xFilter 메서드는 모든 가상 테이블 구현에 필요해요.

2.10. xNext 메서드

int (*xNext)(sqlite3_vtab_cursor*);

xNext 메서드는 가상 테이블 커서xFilter가 시작한 결과 집합의 다음 행으로 진행해요. 이 루틴이 호출될 때 커서가 이미 마지막 행을 가리키고 있으면 커서는 더 이상 유효한 데이터를 가리키지 않고 이후 xEof 메서드 호출은 true(0이 아닌 값)를 반환해야 해요. 커서가 다른 내용 행으로 성공적으로 진행되면 이후 xEof 호출은 false(0)를 반환해야 해요.

이 메서드는 성공하면 SQLITE_OK를, 오류가 발생하면 sqlite 오류 코드를 반환해야 해요.

xNext 메서드는 모든 가상 테이블 구현에 필요해요.

2.11. xColumn 메서드

int (*xColumn)(sqlite3_vtab_cursor*, sqlite3_context*, int N);

SQLite 코어는 현재 행의 N번째 열 값을 찾기 위해 이 메서드를 호출해요. N은 0부터 시작하므로 첫 번째 열은 0으로 번호가 매겨져요. xColumn 메서드는 다음 인터페이스 중 하나를 사용해 결과를 SQLite에 반환할 수 있어요:

xColumn 메서드 구현이 위 함수 중 어떤 것도 호출하지 않으면 열의 값은 SQL NULL로 기본값이 정해져요.

오류를 발생시키려면 xColumn 메서드는 result_text() 메서드 중 하나로 오류 메시지 텍스트를 설정한 다음 적절한 오류 코드를 반환해야 해요. xColumn 메서드는 성공 시 SQLITE_OK를 반환해야 해요.

xColumn 메서드는 모든 가상 테이블 구현에 필요해요.

2.12. xRowid 메서드

int (*xRowid)(sqlite3_vtab_cursor *pCur, sqlite_int64 *pRowid);

이 메서드의 성공적인 호출은 *pRowid가 가상 테이블 커서 pCur이 현재 가리키는 행의 rowid로 채워지게 해요. 이 메서드는 성공 시 SQLITE_OK를 반환해요. 실패 시 적절한 오류 코드를 반환해요.

xRowid 메서드는 모든 가상 테이블 구현에 필요해요.

2.13. xUpdate 메서드

int (*xUpdate)(
  sqlite3_vtab *pVTab,
  int argc,
  sqlite3_value **argv,
  sqlite_int64 *pRowid
);

가상 테이블에 대한 모든 변경은 xUpdate 메서드로 이루어져요. 이 하나의 메서드로 삽입, 삭제, 갱신을 할 수 있어요.

argc 매개변수는 argv 배열의 항목 수를 지정해요. argc 값은 순수 삭제 연산의 경우 1이고, 삽입 또는 대체 또는 갱신의 경우 N+2이며 여기서 N은 테이블의 열 수예요. 앞 문장에서 N은 숨은 열도 포함해요.

모든 argv 항목은 C에서 NULL이 아닌 값을 가지지만 SQL 값 NULL을 포함할 수 있어요. 즉 i가 0과 argc-1 사이일 때 항상 argv[i]!=0이 참이에요. 하지만 sqlite3_value_type(argv[i])==SQLITE_NULL인 경우가 있을 수 있어요.

argv[0] 매개변수는 삭제할 가상 테이블의 행의 rowid예요. argv[0]이 SQL NULL이면 삭제가 발생하지 않아요.

argv[1] 매개변수는 가상 테이블에 삽입할 새 행의 rowid예요. argv[1]이 SQL NULL이면 구현이 새로 삽입된 행의 rowid를 선택해야 해요. 이후 argv[] 항목은 열이 선언된 순서대로 가상 테이블의 열 값을 담아요. 열 수는 xConnect 또는 xCreate 메서드가 sqlite3_declare_vtab() 호출로 만든 테이블 선언과 일치할 거예요. 모든 숨은 열이 포함돼요.

ROWID를 사용하는 가상 테이블에서(WITHOUT ROWID 가상 테이블이 아닌) rowid 없이 삽입할 때(argc>1, argv[1]이 SQL NULL) 구현은 *pRowid를 새로 삽입된 행의 rowid로 설정해야 해요. 이것은 sqlite3_last_insert_rowid() 함수가 반환하는 값이 될 거예요. 다른 모든 경우에 이 값을 설정하는 것은 무해한 no-op이에요. SQLite 엔진은 argc==1이거나 argv[1]이 SQL NULL이 아니면 *pRowid 반환 값을 무시해요.

각 xUpdate 호출은 아래에 보이는 경우 중 하나에 해당할 거예요. **argv[i]**에 대한 참조는 argv[i] 객체 자체가 아니라 argv[i] 객체 안에 담긴 SQL 값을 의미한다는 점에 유의하세요.

argc = 1
argv[0] ≠ NULL

DELETE: rowid 또는 PRIMARY KEY가 argv[0]과 같은 단일 행이 삭제돼요. 삽입은 발생하지 않아요.

argc > 1
argv[0] = NULL

INSERT: argv[2] 및 이후에서 열 값을 가져와 새 행이 삽입돼요. rowid 가상 테이블에서 argv[1]이 SQL NULL이면 새 고유 rowid가 자동으로 생성돼요. WITHOUT ROWID 가상 테이블의 경우 argv[1]은 NULL일 거예요. 그 경우 구현은 argv[2] 및 이후의 적절한 열에서 PRIMARY KEY 값을 가져와야 해요.

argc > 1
argv[0] ≠ NULL
argv[0] = argv[1]

UPDATE: rowid 또는 PRIMARY KEY가 argv[0]인 행이 argv[2] 및 이후 매개변수의 새 값으로 갱신돼요.

argc > 1
argv[0] ≠ NULL
argv[0] ≠ argv[1]

rowid 또는 PRIMARY KEY 변경을 동반한 UPDATE: rowid 또는 PRIMARY KEY가 argv[0]인 행이 argv[1]의 rowid 또는 PRIMARY KEY와 argv[2] 및 이후 매개변수의 새 값으로 갱신돼요. 이것은 SQL 문이 rowid를 갱신할 때 발생해요. 예:

UPDATE table SET rowid=rowid+1 WHERE ...;

xUpdate 메서드는 성공한 경우에만 SQLITE_OK를 반환해야 해요. 실패가 발생하면 xUpdate는 적절한 오류 코드를 반환해야 해요. 실패 시 pVTab->zErrMsg 요소는 선택적으로 sqlite3_mprintf()sqlite3_malloc() 같은 함수를 사용해 SQLite에서 할당된 메모리에 저장된 오류 메시지 텍스트로 대체될 수 있어요.

xUpdate 메서드가 가상 테이블의 어떤 제약을 위반하면(잘못된 데이터 타입 값을 저장하려 하거나, 너무 크거나 너무 작은 값을 저장하려 하거나, 읽기 전용 값을 변경하려 하는 것 등을 포함하되 이에 국한되지 않음) xUpdate는 적절한 오류 코드로 실패해야 해요.

xUpdate 메서드가 UPDATE를 수행 중이면 sqlite3_value_nochange(X)를 사용해 UPDATE 문이 실제로 수정한 가상 테이블의 열이 무엇인지 알아낼 수 있어요. sqlite3_value_nochange(X) 인터페이스는 변경되지 않는 열에 대해 true를 반환해요. 모든 UPDATE에서 SQLite는 먼저 테이블의 각 변경되지 않는 열에 대해 xColumn을 개별적으로 호출해 그 열의 값을 얻어요. xColumn 메서드는 sqlite3_vtab_nochange()를 호출해 열이 SQL 수준에서 변경되지 않았는지 확인할 수 있어요. xColumn이 열이 수정되지 않고 있음을 보면 sqlite3_result_xxxxx() 인터페이스 중 하나로 결과를 설정하지 않고 반환해야 해요. 그래야만 xUpdate 메서드 안에서 sqlite3_value_nochange()가 true가 될 거예요. xColumnsqlite3_result_xxxxx() 인터페이스 중 하나 이상을 호출하면 SQLite는 그것을 열 값의 변경으로 이해하고 xUpdate 안에서 그 열에 대한 sqlite3_value_nochange() 호출은 false를 반환할 거예요.

xUpdate 메서드가 호출될 때 가상 테이블 인스턴스에, 어쩌면 가상 테이블의 행에도 하나 이상의 sqlite3_vtab_cursor 객체가 열려 사용 중일 수 있어요. xUpdate의 구현은 다른 기존 커서가 보던 행을 삭제하거나 수정하려는 시도에 대비해야 해요. 가상 테이블이 그러한 변경을 수용할 수 없으면 xUpdate 메서드는 오류 코드를 반환해야 해요.

xUpdate 메서드는 선택 사항이에요. 가상 테이블의 sqlite3_module에서 xUpdate 포인터가 NULL 포인터이면 가상 테이블은 읽기 전용이에요.

2.14. xFindFunction 메서드

int (*xFindFunction)(
  sqlite3_vtab *pVtab,
  int nArg,
  const char *zName,
  void (**pxFunc)(sqlite3_context*,int,sqlite3_value**),
  void **ppArg
);

이 메서드는 sqlite3_prepare() 동안 가상 테이블 구현에 함수 오버로딩 기회를 주기 위해 호출돼요. 이 메서드는 NULL로 설정될 수 있으며 그 경우 오버로딩이 발생하지 않아요.

함수가 가상 테이블의 열을 첫 번째 인자로 사용할 때 이 메서드가 호출되어 가상 테이블이 함수를 오버로드하고 싶은지 확인해요. 처음 세 매개변수는 입력이에요: 가상 테이블, 함수의 인자 수, 함수 이름. 오버로딩을 원하지 않으면 이 메서드는 0을 반환해요. 함수를 오버로드하려면 이 메서드는 새 함수 구현을 *pxFunc에 쓰고 사용자 데이터를 *ppArg에 쓴 다음 1 또는 SQLITE_INDEX_CONSTRAINT_FUNCTION과 255 사이의 숫자를 반환해요.

역사적으로 xFindFunction()의 반환 값은 0 또는 1이었어요. 0은 함수가 오버로드되지 않았음을, 1은 오버로드되었음을 의미해요. SQLITE_INDEX_CONSTRAINT_FUNCTION 또는 그보다 큰 값을 반환하는 능력은 3.25.0 버전(2018-09-15)에 추가되었어요. xFindFunction이 SQLITE_INDEX_CONSTRAINT_FUNCTION 또는 그보다 큰 값을 반환하면, 그 함수는 두 인자를 취하고 쿼리의 WHERE 절에서 불리언으로 사용될 수 있으며 가상 테이블이 그 함수를 활용해 쿼리 결과를 빠르게 할 수 있다는 뜻이에요. xFindFunction이 SQLITE_INDEX_CONSTRAINT_FUNCTION 또는 그보다 큰 값을 반환할 때 그 반환 값은 xBestIndex()에 전달되는 제약 중 하나의 sqlite3_index_info.aConstraint.op 값이 돼요. 함수의 첫 번째 인자는 제약의 aConstraint[].iColumn 필드가 식별한 열이고, 함수의 두 번째 인자는 xFilter()로 전달될 값(aConstraintUsage[].argvIndex 값이 설정된 경우) 또는 sqlite3_vtab_rhs_value()가 반환한 값이에요.

Geopoly 모듈SQLITE_INDEX_CONSTRAINT_FUNCTION을 사용해 성능을 개선하는 가상 테이블의 예예요. Geopoly의 xFindFunction() 메서드는 geopoly_overlap() SQL 함수에 대해 SQLITE_INDEX_CONSTRAINT_FUNCTION을 반환하고 geopoly_within() SQL 함수에 대해 SQLITE_INDEX_CONSTRAINT_FUNCTION+1을 반환해요. 이것은 다음과 같은 쿼리에 대한 검색 최적화를 허용해요:

SELECT * FROM geopolytab WHERE geopoly_overlap(_shape, $query_polygon);
SELECT * FROM geopolytab WHERE geopoly_within(_shape, $query_polygon);

중위(infix) 함수(LIKE, GLOB, REGEXP, MATCH)는 인자 순서를 뒤집는다는 점에 주의하세요. 따라서 "like(A,B)"는 보통 "B like A"와 같게 동작할 거예요. 하지만 xFindFunction()은 항상 첫 번째 논리적 인자가 아니라 가장 왼쪽 인자를 봐요. 따라서 "B like A" 형식의 경우 SQLite는 왼쪽 피연산자 "B"를 보고 그 피연산자가 가상 테이블 열이면 그 가상 테이블의 xFindFunction() 메서드를 호출해요. 하지만 "like(A,B)" 형식이 대신 사용되면 SQLite는 A 항이 가상 테이블의 열인지 확인하고 그렇다면 열 A의 가상 테이블에 대해 xFindFunction() 메서드를 호출해요.

이 루틴이 반환한 함수 포인터는 첫 번째 매개변수에 주어진 sqlite3_vtab 객체의 수명 동안 유효해야 해요.

2.15. xBegin 메서드

int (*xBegin)(sqlite3_vtab *pVTab);

이 메서드는 가상 테이블에서 트랜잭션을 시작해요. 이 메서드는 선택 사항이에요. sqlite3_module의 xBegin 포인터는 NULL일 수 있어요.

이 메서드 다음에는 항상 xCommit 또는 xRollback 메서드 호출 하나가 따라와요. 가상 테이블 트랜잭션은 중첩되지 않으므로, xCommit 또는 xRollback 호출이 개입하지 않고는 단일 가상 테이블에서 xBegin 메서드가 두 번 이상 호출되지 않을 거예요. 다른 메서드에 대한 여러 호출이 xBegin과 해당 xCommit 또는 xRollback 사이에 발생할 수 있고 아마 발생할 거예요.

2.16. xSync 메서드

int (*xSync)(sqlite3_vtab *pVTab);

이 메서드는 가상 테이블에서 2단계 커밋(two-phase commit)의 시작을 알려요. 이 메서드는 선택 사항이에요. sqlite3_module의 xSync 포인터는 NULL일 수 있어요.

이 메서드는 xBegin 호출 후, xCommit 또는 xRollback 전에만 호출돼요. 2단계 커밋을 구현하기 위해, 어떤 가상 테이블에서 xCommit 메서드를 호출하기 전에 모든 가상 테이블의 xSync 메서드가 호출돼요. xSync 메서드 중 하나라도 실패하면 전체 트랜잭션이 롤백돼요.

2.17. xCommit 메서드

int (*xCommit)(sqlite3_vtab *pVTab);

이 메서드는 가상 테이블 트랜잭션을 커밋하게 해요. 이 메서드는 선택 사항이에요. sqlite3_module의 xCommit 포인터는 NULL일 수 있어요.

이 메서드에 대한 호출은 항상 이전의 xBeginxSync 호출 다음에 와요.

2.18. xRollback 메서드

int (*xRollback)(sqlite3_vtab *pVTab);

이 메서드는 가상 테이블 트랜잭션을 롤백하게 해요. 이 메서드는 선택 사항이에요. sqlite3_module의 xRollback 포인터는 NULL일 수 있어요.

이 메서드에 대한 호출은 항상 이전의 xBegin 호출 다음에 와요.

2.19. xRename 메서드

int (*xRename)(sqlite3_vtab *pVtab, const char *zNew);

이 메서드는 가상 테이블에 새 이름이 주어질 것임을 가상 테이블 구현에 알려요. 이 메서드가 SQLITE_OK를 반환하면 SQLite가 테이블 이름을 바꿔요. 이 메서드가 오류 코드를 반환하면 이름 바꾸기가 방지돼요.

xRename 메서드는 선택 사항이에요. 생략하면 가상 테이블은 ALTER TABLE RENAME 명령으로 이름을 바꿀 수 없어요.

이 메서드를 호출하기 전에 PRAGMA legacy_alter_table 설정이 활성화되고, 이 메서드가 끝난 후 legacy_alter_table 값이 복원돼요. 이것은 그림자 테이블을 사용하는 가상 테이블이 올바르게 동작하는 데 필요해요. 그러한 가상 테이블에서는 그림자 테이블이 새 가상 테이블 이름과 일치하도록 이름을 바꿔야 해요. legacy_alter_format이 꺼져 있으면 xRename 메서드가 그림자 테이블의 이름을 바꿀 때마다 가상 테이블에 대해 xConnect 메서드가 호출될 거예요.

2.20. xSavepoint, xRelease, xRollbackTo 메서드

int (*xSavepoint)(sqlite3_vtab *pVtab, int);
int (*xRelease)(sqlite3_vtab *pVtab, int);
int (*xRollbackTo)(sqlite3_vtab *pVtab, int);

이 메서드는 가상 테이블 구현에 중첩 트랜잭션을 구현할 기회를 제공해요. 이 메서드는 항상 선택 사항이며 SQLite 3.7.7 버전(2011-06-23) 이상에서만 호출될 거예요.

xSavepoint(X,N)이 호출되면 가상 테이블 X에 현재 상태를 세이브포인트 N으로 저장해야 한다는 신호예요. 이후의 xRollbackTo(X,R) 호출은 가상 테이블의 상태가 xSavepoint(X,R)이 마지막으로 호출되었을 때의 상태로 돌아가야 한다는 뜻이에요. xRollbackTo(X,R) 호출은 N>R인 모든 세이브포인트를 무효화해요. 무효화된 세이브포인트 중 어떤 것도 xSavepoint() 호출로 재초기화되기 전에는 롤백되거나 해제되지 않을 거예요. xRelease(X,M) 호출은 N>=M인 모든 세이브포인트를 무효화해요.

xSavepoint(), xRelease(), xRollbackTo() 메서드는 xBegin()과 xCommit() 또는 xRollback() 호출 사이에서만 호출될 거예요.

2.21. xShadowName 메서드

일부 가상 테이블 구현(예: FTS3, FTS5, RTREE)은 내용을 저장하기 위해 실제(비가상) 데이터베이스 테이블을 사용해요. 예를 들어 내용이 FTS3 가상 테이블에 삽입되면 데이터는 결국 "٪_content", "٪_segdir", "٪_segments", "٪_stat", "٪_docsize"라는 실제 테이블에 저장돼요. 여기서 "٪"는 원래 가상 테이블의 이름이에요. 가상 테이블의 내용을 저장하는 이 보조 실제 테이블을 "그림자 테이블(shadow table)"이라고 불러요. 추가 정보는 (1), (2), (3)를 참고하세요.

xShadowName 메서드는 SQLite가 특정 실제 테이블이 실제로 가상 테이블의 그림자 테이블인지 결정할 수 있게 하기 위해 존재해요.

SQLite는 다음이 모두 참일 때 실제 테이블을 그림자 테이블로 이해해요:

  • 테이블 이름에 "_" 문자가 하나 이상 포함된다.
  • 마지막 "_" 앞의 이름 부분이 CREATE VIRTUAL TABLE로 만들어진 가상 테이블의 이름과 정확히 일치한다. (그림자 테이블은 동명 가상 테이블테이블 값 함수에 대해서는 인식되지 않아요.)
  • 가상 테이블이 xShadowName 메서드를 포함한다.
  • xShadowName 메서드가 입력이 테이블 이름의 마지막 "_" 뒤 부분일 때 true를 반환한다.

SQLite가 테이블을 그림자 테이블로 인식하고 SQLITE_DBCONFIG_DEFENSIVE 플래그가 설정되면 그림자 테이블은 일반 SQL 문에 대해 읽기 전용이에요. 그림자 테이블은 여전히 쓸 수 있지만, 어떤 가상 테이블 구현의 메서드 중 하나 안에서 호출된 SQL로만 쓸 수 있어요.

xShadowName 메서드의 핵심은 악의적인 SQL로부터 그림자 테이블 내용이 손상되는 것을 보호하는 것이에요. 그림자 테이블을 사용하는 모든 가상 테이블 구현은 손상된 그림자 테이블 내용을 감지하고 대처할 수 있어야 해요. 하지만 특정 가상 테이블 구현의 버그로 인해 의도적으로 손상된 그림자 테이블이 충돌이나 다른 오작동을 일으킬 수 있어요. xShadowName 메커니즘은 일반 SQL 문이 의도적으로 그림자 테이블을 손상시키는 것을 방지해 제로데이 익스플로잇을 피하려 해요.

그림자 테이블은 기본적으로 읽기/쓰기예요. 그림자 테이블은 sqlite3_db_config()SQLITE_DBCONFIG_DEFENSIVE 플래그가 설정될 때만 읽기 전용이 돼요. 하위 호환성을 유지하기 위해 그림자 테이블은 기본적으로 읽기/쓰기여야 해요. 예를 들어 CLI.dump 명령이 생성하는 SQL 텍스트는 그림자 테이블에 직접 써요.

2.22. xIntegrity 메서드

sqlite3_module의 iVersion이 4 이상이고 xIntegrity 메서드가 NULL이 아니면 PRAGMA integrity_checkPRAGMA quick_check 명령이 그 처리의 일부로 xIntegrity를 호출할 거예요. xIntegrity 메서드가 다섯 번째 매개변수에 오류 메시지 문자열을 쓰면 PRAGMA integrity_check가 그 오류를 출력의 일부로 보고할 거예요. 즉 xIntegrity 메서드는 PRAGMA integrity_check 명령이 가상 테이블에 저장된 내용의 무결성을 검증할 수 있게 해줘요.

xIntegrity 메서드는 다섯 개의 매개변수로 호출돼요:

  • pVTab → 검사 중인 가상 테이블인 sqlite3_vtab 객체에 대한 포인터.
  • zSchema → 가상 테이블이 정의된 스키마("main", "temp" 등)의 이름.
  • zTabName → 가상 테이블의 이름.
  • mFlags → "integrity_check"인지 "quick_check"인지 나타내는 플래그. 현재 이 매개변수는 항상 0 또는 1이지만, 미래의 SQLite 버전은 정수의 다른 비트를 사용해 추가 처리 옵션을 나타낼 수 있어요.
  • pzErr → 이 매개변수는 NULL로 초기화된 "char*"를 가리켜요. xIntegrity() 구현은 문제를 찾으면 *pzErr가 sqlite3_malloc() 또는 그에 상응하는 것에서 얻은 오류 문자열을 가리키게 해야 해요.

xIntegrity 메서드는 보통 SQLITE_OK를 반환해야 해요. - 가상 테이블의 내용에서 문제를 찾더라도 마찬가지예요. 그 외의 오류 코드는 xIntegrity 메서드 자체가 가상 테이블 내용을 평가하려다 문제를 겪었음을 의미해요. 예를 들어 FTS5의 역방향 색인이 내부적으로 불일치함이 발견되면 xIntegrity 메서드는 pzErr 매개변수에 적절한 오류 메시지를 쓰고 SQLITE_OK를 반환해야 해요. 하지만 xIntegrity 메서드가 메모리 부족으로 가상 테이블 내용 평가를 완료하지 못하면 SQLITE_NOMEM을 반환해야 해요.

오류 메시지가 생성되면 오류 메시지 문자열을 담을 공간을 sqlite3_malloc64() 또는 그에 상응하는 것에서 얻어야 해요. 오류 메시지 문자열의 소유권은 xIntegrity가 반환할 때 SQLite 코어로 넘어가요. 코어는 오류 메시지를 다 쓴 후 sqlite3_free()가 호출되어 메모리를 회수하게 할 거예요. xIntegrity 메서드를 호출하는 PRAGMA integrity_check 명령은 반환된 오류 메시지를 변경하지 않아요. xIntegrity 메서드 자체가 메시지의 일부로 가상 테이블의 이름을 포함해야 해요. zSchema와 zName 매개변수는 그것을 더 쉽게 하기 위해 제공돼요.

mFlags 매개변수는 현재 불리언 값(0 또는 1)으로, xIntegrity 메서드가 PRAGMA integrity_check(mFlags==0) 때문인지 PRAGMA quick_check(mFlags==1) 때문인지 나타내요. 일반적으로 xIntegrity 메서드는 선형 시간 안에 할 수 있는 유효성 검사는 무엇이든 해야 하지만, (mFlags&1)==0일 때만 초선형 시간이 필요한 검사를 해야 해요. 미래의 SQLite 버전은 mFlags 매개변수의 상위 비트를 사용해 추가 처리 옵션을 나타낼 수 있어요.

xIntegrity 메서드에 대한 지원은 SQLite 3.44.0 버전(2023-11-01)에 추가되었어요. 같은 릴리스에서 xIntegrity 메서드가 FTS3, FTS5, RTREE 같은 많은 내장 가상 테이블에 추가되어, 이후 PRAGMA integrity_check를 실행할 때 그 테이블의 내용이 자동으로 일관성 검사되게 했어요.

더 알아보기 (Learn more)