文章

SQLite3 源码分析-sqlite_schema(sqlite_master) 的创建

SQLite3 源码分析-sqlite_schema(sqlite_master) 的创建

前记

在 sqlite 初始化的过程中,会首先创建 sqlite_schema(sqlite_master) 这个表,然后执行 select * from main.sqlite_master 语句,将数据库中的表,索引,视图,触发器等信息加载到 sqlite->aDb->pSchema 中。

整体结构图

flowchart LR
  A(["sqlite init"]) --> B(["创建 sqlite_master"]) --> C(["读取 sqlite_master"]) --> D(["加载数据到内存"])

代码详解

根据 SQLite3 的数据格式 我们可以看到,在数据库文件中的第一个页面存在一个名字叫做 sqlite_schema 的表,这个表中会记录 sqlite 数据库中所有的表,索引,视图,以及触发器。这个表会在我们第一次对数据库进行操作的时候进行创建。这个表就相当于数据库中的一个元信息表,我们对数据库的任何 DDL(Data Definition Language,数据定义语言) 操作之前,都需要检查一下,这个数据库中是不是已经存在了相同名字的表。具体的函数就是在 sqlite3ReadSchema

sqlite3ReadSchema

1
2
3
4
5
int sqlite3ReadSchema(Parse *pParse){
  sqlite3 *db = pParse->db;
  if( !db->init.busy ){
    rc = sqlite3Init(db, &pParse->zErrMsg);
  }

在这个函数中我们可以看到如果 db.init.busy 来判断数据库当前是否在初始化中,如果没有在初始化,那么我们就进行初始化。

sqlite3Init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int sqlite3Init(sqlite3 *db, char **pzErrMsg){
  int i, rc;
  /* Do the main schema first */
  if( !DbHasProperty(db, 0, DB_SchemaLoaded) ){
    rc = sqlite3InitOne(db, 0, pzErrMsg, 0);
    if( rc ) return rc;
  }
  for(i=db->nDb-1; i>0; i--){
    assert( i==1 || sqlite3BtreeHoldsMutex(db->aDb[i].pBt) );
    if( !DbHasProperty(db, i, DB_SchemaLoaded) ){
      rc = sqlite3InitOne(db, i, pzErrMsg, 0);
      if( rc ) return rc;
    }
  }
}

在这个函数中,我们看到主要分为两个部分,一个是检查当前数据库是否已经将数据库中的表,索引等信息读取到内存中的哈希表,这个通过 DB_SchemaLoaded 来判断。如果不是最新的,我们就通过 sqlite3InitOne 来读取。在第二个 for 循环中功能也是一样的,用来读取数据库中的元数据。

sqlite 中有两个数据库,一个是 main 数据库,用于储存长期数据,一个是 temp 数据库,用于存储一些临时的对象。

sqlite3InitOne

现在我们来看一下 sqlite3InitOne 函数:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
int sqlite3InitOne(sqlite3 *db, int iDb, char **pzErrMsg, u32 mFlags){
  int rc;
  int i;

  Db *pDb;
  char const *azArg[6];
  int meta[5];
  InitData initData;
  const char *zSchemaTabName;
  int openedTransaction = 0;
  int mask = ((db->mDbFlags & DBFLAG_EncodingFixed) | ~DBFLAG_EncodingFixed);

  db->init.busy = 1;

  azArg[0] = "table";
  azArg[1] = zSchemaTabName = SCHEMA_TABLE(iDb);
  azArg[2] = azArg[1];
  azArg[3] = "1";
  azArg[4] = "CREATE TABLE x(type text,name text,tbl_name text,"
                            "rootpage int,sql text)";
  azArg[5] = 0;
  initData.db = db;
  initData.iDb = iDb;
  initData.rc = SQLITE_OK;
  initData.pzErrMsg = pzErrMsg;
  initData.mInitFlags = mFlags;
  initData.nInitRow = 0;
  initData.mxPage = 0;
  sqlite3InitCallback(&initData, 5, (char **)azArg, 0);
  
  /* Create a cursor to hold the database open
  */
  pDb = &db->aDb[iDb];
  if( pDb->pBt==0 ){
    assert( iDb==1 );
    DbSetProperty(db, 1, DB_SchemaLoaded);
    rc = SQLITE_OK;
    goto error_out;
  }

  sqlite3BtreeEnter(pDb->pBt);
  if( sqlite3BtreeTxnState(pDb->pBt)==SQLITE_TXN_NONE ){
    rc = sqlite3BtreeBeginTrans(pDb->pBt, 0, 0);
    if( rc!=SQLITE_OK ){
      sqlite3SetString(pzErrMsg, db, sqlite3ErrStr(rc));
      goto initone_error_out;
    }
    openedTransaction = 1;
  }
  
  for(i=0; i<ArraySize(meta); i++){
    sqlite3BtreeGetMeta(pDb->pBt, i+1, (u32 *)&meta[i]);
  }
  if( (db->flags & SQLITE_ResetDatabase)!=0 ){
    memset(meta, 0, sizeof(meta));
  }
  pDb->pSchema->schema_cookie = meta[BTREE_SCHEMA_VERSION-1];

  pDb->pSchema->enc = ENC(db);

  pDb->pSchema->file_format = (u8)meta[BTREE_FILE_FORMAT-1];
  if( pDb->pSchema->file_format==0 ){
    pDb->pSchema->file_format = 1;
  }
  if( pDb->pSchema->file_format>SQLITE_MAX_FILE_FORMAT ){
    sqlite3SetString(pzErrMsg, db, "unsupported file format");
    rc = SQLITE_ERROR;
    goto initone_error_out;
  }
  if( iDb==0 && meta[BTREE_FILE_FORMAT-1]>=4 ){
    db->flags &= ~(u64)SQLITE_LegacyFileFmt;
  }

  assert( db->init.busy );
  initData.mxPage = sqlite3BtreeLastPage(pDb->pBt);
  {
    char *zSql;
    zSql = sqlite3MPrintf(db, 
        "SELECT*FROM\"%w\".%s ORDER BY rowid",
        db->aDb[iDb].zDbSName, zSchemaTabName);
      rc = sqlite3_exec(db, zSql, sqlite3InitCallback, &initData, 0);
  }
  db->init.busy = 0;
  return rc;
}

在这个函数中,我们需要重点看一下:

  1. sqliteInitCallback 这个函数
  2. 调用 sqlite_exec 这个函数,这个会指定 sqliteInitCallback 作为回调函数

sqliteInitCallback

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
int sqlite3InitCallback(void *pInit, int argc, char **argv, char **NotUsed){
  InitData *pData = (InitData*)pInit;
  sqlite3 *db = pData->db;
  int iDb = pData->iDb;

  if( argv[3]==0 ){
    corruptSchema(pData, argv, 0);
  }else if( argv[4]
         && 'c'==sqlite3UpperToLower[(unsigned char)argv[4][0]]
         && 'r'==sqlite3UpperToLower[(unsigned char)argv[4][1]] ){
    int rc;
    u8 saved_iDb = db->init.iDb;
    sqlite3_stmt *pStmt;
    db->init.iDb = iDb;
    
    pStmt = 0;
    TESTONLY(rcp = ) sqlite3Prepare(db, argv[4], -1, 0, 0, &pStmt, 0);
    rc = db->errCode;
    assert( (rc&0xFF)==(rcp&0xFF) );
    db->init.iDb = saved_iDb;
    /* assert( saved_iDb==0 || (db->mDbFlags & DBFLAG_Vacuum)!=0 ); */
    
    db->init.azInit = sqlite3StdType; /* Any array of string ptrs will do */
    sqlite3_finalize(pStmt);
  }
  return 0;
}

在这个 argv[4] 中是这个 sql 语句,这个就是我们刚刚创建那个 CREATE TABLE x(type text, name text,tbl_name text), 这里就进入到我们判断第一个字符是不是 ‘c’ ,第二个字符是不是 ‘r’ ,如果是的话,说明就是一个创建表的语句(v这里为什么不是创建索引或者其他之类的呢,因为这是在初始化的过程,记录元信息表才是最重要的)。接下来我们就会进入到 sqlite3Prepare

sqlite3Prepare

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
/*
** Compile the UTF-8 encoded SQL statement zSql into a statement handle.
*/
static int sqlite3Prepare(
  sqlite3 *db,              /* Database handle. */
  const char *zSql,         /* UTF-8 encoded SQL statement. */
  int nBytes,               /* Length of zSql in bytes. */
  u32 prepFlags,            /* Zero or more SQLITE_PREPARE_* flags */
  Vdbe *pReprepare,         /* VM being reprepared */
  sqlite3_stmt **ppStmt,    /* OUT: A pointer to the prepared statement */
  const char **pzTail       /* OUT: End of parsed string */
){
  int rc = SQLITE_OK;       /* Result code */
  int i;                    /* Loop counter */
  Parse sParse;             /* Parsing context */

  if( nBytes>=0 && (nBytes==0 || zSql[nBytes-1]!=0) ){
    ...
  }else{
    sqlite3RunParser(&sParse, zSql);
  }
  assert( 0==sParse.nQueryLoop );

  if( pzTail ){
    *pzTail = sParse.zTail;
  }
  return rc;
}

这里我们调用 sqlite3RunParser 函数进行 sql 解析,等解析完成后,我们就会生成调用”编译器” 来生成字节码,这一部分我们后续再说。进入到这个函数以后,就会调用由 parse.y 这个文件生成的语法生成器来解析 sql 语法。

这里我们引入部分 parse.y 的文件内容来进行说明:

create_table_args ::= LP columnlist conslist_opt(X) RP(E) table_option_set(F). {
  sqlite3EndTable(pParse,&X,&E,F,0);
}

这里我们可以看到创建一个表的解析过程为左括号(LP), 数据列列表(columnlist),限制选项(conslist_opt),右括号(RP), 建表的一些属性(table_option_set) ,最后由 sqlite3EndTable 这个函数来创建一个表。接下来我们来看一下这个函数

sqlite3EndTable

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
34
35
36
37
38
39
40
void sqlite3EndTable(
  Parse *pParse,          /* Parse context */
  Token *pCons,           /* The ',' token after the last column defn. */
  Token *pEnd,            /* The ')' before options in the CREATE TABLE */
  u32 tabOpts,            /* Extra table options. Usually 0. */
  Select *pSelect         /* Select from a "CREATE ... AS SELECT" */
){
    Table *p;                 /* The new table */
    sqlite3 *db = pParse->db; /* The database connection */
    int iDb;                  /* Database in which the table lives */
    Index *pIdx;              /* An implied index of the table */


    /* Compute the complete text of the CREATE statement */
    if( pSelect ){
      zStmt = createTableStmt(db, p);
    }else{
      Token *pEnd2 = tabOpts ? &pParse->sLastToken : pEnd;
      n = (int)(pEnd2->z - pParse->sNameToken.z);
      if( pEnd2->z[0]!=';' ) n += pEnd2->n;
      zStmt = sqlite3MPrintf(db, 
          "CREATE %s %.*s", zType2, n, pParse->sNameToken.z
      );
    }

  /* Add the table to the in-memory representation of the database.
  */
  if( db->init.busy ){
    Table *pOld;
    Schema *pSchema = p->pSchema;
    assert( sqlite3SchemaMutexHeld(db, iDb, 0) );
    assert( HasRowid(p) || p->iPKey<0 );
    pOld = sqlite3HashInsert(&pSchema->tblHash, p->zName, p);
    if( pOld ){
      assert( p==pOld );  /* Malloc must have failed inside HashInsert() */
      sqlite3OomFault(db);
      return;
    }
  }
}

这里我们主要看一下有一个 ` if( pSelect ) 语句,这里用来判断是否是 CREATE TABLE AS SELECT 这种形式的语句。这里我们不做说明, 然后我们进入 else 语句块,这里实际是组成了 CREATE TABLE master.sqlite_schema 的语句,然后我们就可以在下面的的if 判断中将数据表插入到内存中。sqlite3HashInsert` 这个函数用来将数据表插入到内存中。

这个函数的原型为 sqlite3HashInsert(Hash *pH, const char *pKey, void *data) ,目的就是将表,索引等插入到 Hash 表中。 修改的数据对象为 db.aDb.pSchema.tblHash.

后记

第一篇 sqlite3 的源码解析,已经写完了,但是感觉写的并不好,逻辑不够清晰,代码解释有点云里雾里,不过加油吧。💪。

本文由作者按照 CC BY 4.0 进行授权