手写MySQL(九) TBM模块篇-字段解析与表管理

代码地址:GitHub - zhangqingqing24630/MySQL-

此项目来源于何人听我楚狂声

 

 

目录

 

SQL 解析器

实现方法

语句切割

测试代码

语句解析

字段与表管理

字段管理Field

存储结构

读取字段

创建字段

表管理Table

存储结构

读取表

解析表

创建表

Update操作

insert操作

read操作

计算 Where 条件的范围

表的启动管理

TableManager的实现

测试代码


 

TBM也是最后一个模块,实现了对字段结构和表结构的管理,其与其他模块的依赖关系如下

8a45ac85c4d8484d9b41a070d91398ad.jpeg

 

SQL 解析器

实现的 SQL 语句语法如下:

<begin statement>
    begin [isolation level (read committed|repeatable read)]
        begin isolation level read committed

<commit statement>
    commit

<abort statement>
    abort

<create statement>
    create table <table name>
    <field name> <field type>
    <field name> <field type>
    ...
    <field name> <field type>
    [(index <field name list>)]
        create table students
        id int32,
        name string,
        age int32,
        (index id name)

<drop statement>
    drop table <table name>
        drop table students

<select statement>
    select (*|<field name list>) from <table name> [<where statement>]
        select * from student where id = 1
        select name from student where id > 1 and id < 4
        select name, age, id from student where id = 12

<insert statement>
    insert into <table name> values <value list>
        insert into student values 5 "Zhang Yuanjia" 22

<delete statement>
    delete from <table name> <where statement>
        delete from student where name = "Zhang Yuanjia"

<update statement>
    update <table name> set <field name>=<value> [<where statement>]
        update student set name = "ZYJ" where id = 5

<where statement>
    where <field name> (>|<|=) <value> [(and|or) <field name> (>|<|=) <value>]
        where age > 10 or age < 3

<field name> <table name>
    [a-zA-Z][a-zA-Z0-9_]*

<field type>
    int32 int64 string

实现方法

语句切割

parser 包的 Tokenizer 类,对语句进行逐字节解析,根据空白符或者上述词法规则,将语句切割成多个 token。对外提供了 peek()pop() 方法方便取出 Token 进行解析。

其解析规则如下

1 取出完整的字符串

2 取出引号包起来的内容

3 取出符号,如> < =

其具体实现代码为

private String nextMetaState() throws Exception {
        while(true) {
            //遍历字节数组,直至遍历完,取出
            Byte b = peekByte();
            if(b == null) {
                return "";
            }
            //遇到空格或回车,忽略,继续遍历
            if(!isBlank(b)) {
                break;
            }
            popByte();//pos++;
        }
        byte b = peekByte();
        //如果b是<>=*,这些符号,取出符号
        if(isSymbol(b)) {
            popByte();
            return new String(new byte[]{b});
        //如果b是单引号或者双引号,找到单引号双引号包起来的内容
        } else if(b == '"' || b == '\'') {
            return nextQuoteState();
        //如果b是数字或字母,找到完整的数字或字符串
        } else if(isAlphaBeta(b) || isDigit(b)) {
            return nextTokenState();
        } else {
            err = Error.InvalidCommandException;
            throw err;
        }
    }

测试代码

public class test {
    public static void main(String[] args) throws Exception {
        String ss=" insert into student values 5 \"xiaohong\" 22";
        byte[]b=ss.getBytes(StandardCharsets.UTF_8);
        Tokenizer tokenizer = new Tokenizer(b);
        for(int i=0;i<b.length;i++){
            String token = tokenizer.peek();
            System.out.println(token);
            tokenizer.pop();
        }
    }
}

解析出的字段为  

insert
into
student
values
5
xiaohong
22

语句解析

解析过程是根据解析出的第一个 Token 来区分语句类型,并分别做处理

Tokenizer tokenizer = new Tokenizer(statement);
        String token = tokenizer.peek();
        tokenizer.pop();
        Object stat = null;
        Exception statErr = null;
        try {
            switch(token) {
                case "begin":
                    stat = parseBegin(tokenizer);
                    break;
                case "commit":
                    stat = parseCommit(tokenizer);
                    break;
                case "abort":
                    stat = parseAbort(tokenizer);
                    break;
                case "create":
                    stat = parseCreate(tokenizer);
                    break;
                case "drop":
                    stat = parseDrop(tokenizer);
                    break;
                case "select":
                    stat = parseSelect(tokenizer);
                    break;
                case "insert":
                    stat = parseInsert(tokenizer);
                    break;
                case "delete":
                    stat = parseDelete(tokenizer);
                    break;
                case "update":
                    stat = parseUpdate(tokenizer);
                    break;
                case "show":
                    stat = parseShow(tokenizer);
                    break;
                default:
                    throw Error.InvalidCommandException;
            }
        } catch(Exception e) {
            statErr = e;
        }

以解析插入的SQL 语句为例,需要解析出要插入的表名,和要插入的字段。如果解析出的字段不满足规则,报错。

以insert为例解析出的字段必须满足

insert into 表名(自定 格式 以字母开头) values value(自定)

private static Insert parseInsert(Tokenizer tokenizer) throws Exception {
        Insert insert = new Insert();
        //比如解析insert的下一句一定得是into
        if(!"into".equals(tokenizer.peek())) {
            throw Error.InvalidCommandException;
        }
        tokenizer.pop();
        //解析出表名,不满足规则报错
        String tableName = tokenizer.peek();
        if(!isName(tableName)) {
            throw Error.InvalidCommandException;
        }
        insert.tableName = tableName;
        tokenizer.pop();
        //下一个字段必须是values
        if(!"values".equals(tokenizer.peek())) {
            throw Error.InvalidCommandException;
        }
        //要插入的值
        List<String> values = new ArrayList<>();
        while(true) {
            tokenizer.pop();
            String value = tokenizer.peek();
            if("".equals(value)) {
                break;
            } else {
                values.add(value);
            }
        }
        insert.values = values.toArray(new String[values.size()]);

        return insert;
    }

字段与表管理

字段管理Field

存储结构

由于 TBM 基于 VM的基础之上的,单个字段信息和表信息都是直接保存在 Entry 中,即保存在VM结构的【data】部分。Entry的存储结构为

[FieldName][TypeName][IndexUid]
字段名      字段类型    字段根节点uid

 这里 FieldName 和 TypeName,以及后面要介绍的TableName,存储的都是字节形式的字符串。这里规定一个字符串的存储方式,以明确其存储边界。

[StringLength][StringData]

TypeName 为字段的类型,限定为 int32、int64 和 string 类型。如果这个字段有索引,那个 IndexUID 指向了索引二叉树的根,否则该字段为 0。

即,如果该字段建立了索引,则以该字段会建立一个B+树。

为了不容易混淆,我们总结一下前面介绍过的存储结果。

DataItem的存储结构为

[ValidFlag][Size][Data]

VM的存储结构为一个Entry,存储在DataItem的【Data】中,Entry的存储结构为

[XMin][XMax][Data]

IM存储结构为一个Entry,存储在DataItem的【Data】中,Entry的存储结构为

[LeafFlag][KeyNumber][SiblingUid]
占1个字节          2         8
[Son0][Key0][Son1][Key1]...[SonN][KeyN]
8      8     8

 

重新回到TBM模块上来

读取字段

根据这个结构,通过一个 UID 从表中读取字段并解析如下:

public static Field loadField(Table tb, long uid) {
        byte[] raw = null;
        try {
            //读取一个entry.data()
            raw = ((TableManagerImpl)tb.tbm).vm.read(TransactionManagerImpl.SUPER_XID, uid);
        } catch (Exception e) {
            Panic.panic(e);
        }
        assert raw != null;
        return new Field(uid, tb).parseSelf(raw);
 }


private Field parseSelf(byte[] raw) {
        int position = 0;
        ParseStringRes res = Parser.parseString(raw);
        //解析出字段名
        fieldName = res.str;
        position += res.next;
        res = Parser.parseString(Arrays.copyOfRange(raw, position, raw.length));
        //解析出字段类型
        fieldType = res.str;
        position += res.next;
        //该字段根节点的索引
        this.index = Parser.parseLong(Arrays.copyOfRange(raw, position, position+8));
        if(index != 0) {
            try {
                bt = BPlusTree.load(index, ((TableManagerImpl)tb.tbm).dm);
            } catch(Exception e) {
                Panic.panic(e);
            }
        }
        return this;
    }

创建字段

创建一个字段的方法类似,将相关的信息通过 VM 持久化即可:

//创建字段
public static Field createField(Table tb, long xid, String fieldName, String fieldType, boolean indexed) throws Exception {
        typeCheck(fieldType);
        Field f = new Field(tb, fieldName, fieldType, 0);
        //有索引
        if(indexed) {
            //生成一个空的根节点   ,返回存放根节点的uid
            long index = BPlusTree.create(((TableManagerImpl)tb.tbm).dm);
            以该uid为bootUid的节点为根节点建立一个B+树
            BPlusTree bt = BPlusTree.load(index, ((TableManagerImpl)tb.tbm).dm);
            f.index = index;
            f.bt = bt;
        }
        f.persistSelf(xid);
        return f;
    }
//创建一个字段的方法类似,将相关的信息通过 VM 持久化即可
private void persistSelf(long xid) throws Exception {
    byte[] nameRaw = Parser.string2Byte(fieldName);
    byte[] typeRaw = Parser.string2Byte(fieldType);
    byte[] indexRaw = Parser.long2Byte(index);
    this.uid = ((TableManagerImpl)tb.tbm).vm.insert(xid, Bytes.concat(nameRaw, typeRaw, indexRaw));
}

表管理Table

存储结构

一个数据库中存在多张表,TBM 使用链表的形式将其组织起来,每一张表都保存一个指向下一张表的 UID。表的二进制结构如下:

[TableName][NextTable]
表名        下一张表的uid
[Field1Uid][Field2Uid]...[FieldNUid]

读取表

根据 UID 从 Entry 中读取表数据的过程和读取字段的过程类似。

//根据uid从已有文件中加载表
    public static Table loadTable(TableManager tbm, long uid) {
        byte[] raw = null;
        try {
            raw = ((TableManagerImpl)tbm).vm.read(TransactionManagerImpl.SUPER_XID, uid);
        } catch (Exception e) {
            Panic.panic(e);
        }
        assert raw != null;
        Table tb = new Table(tbm, uid);
        return tb.parseSelf(raw);
    }

解析表

表的解析过程如下,由于uid的字节数是固定的(8个),所以于是无需保存字段的个数。

1 解析表名

2 解析下一个表的uid

3 解析表中的所有字段,且创建字段,把所有的字段存储在一个fields中

private Table parseSelf(byte[] raw) {
        int position = 0;
        ParseStringRes res = Parser.parseString(raw);
        //表名
        name = res.str;
        position += res.next;
        //下一个表的uid
        nextUid = Parser.parseLong(Arrays.copyOfRange(raw, position, position+8));
        position += 8;
        //字段的uid
        while(position < raw.length) {
            long uid = Parser.parseLong(Arrays.copyOfRange(raw, position, position+8));
            position += 8;
            fields.add(Field.loadField(this, uid));
        }
        return this;
    }

创建表

常常用于插入新表时,用Create存储一张表的信息。
public class Create {
    public String tableName;//表名
    public String[] fieldName;//存储的字段名
    public String[] fieldType;//存储的字段类型
    public String[] index;//存储的索引
}

创建表时,需要把表中所有的字段信息存储到dm中

//创建表,用于插入新表时使用
    //nextUid原来第一张表的uid
    //xid插入新表的xid
    //要插入新表的所有信息
    public static Table createTable(TableManager tbm, long nextUid, long xid, Create create) throws Exception {
        Table tb = new Table(tbm, create.tableName, nextUid);
        for(int i = 0; i < create.fieldName.length; i ++) {
            String fieldName = create.fieldName[i];
            String fieldType = create.fieldType[i];
            boolean indexed = false;
            for(int j = 0; j < create.index.length; j ++) {
                if(fieldName.equals(create.index[j])) {
                    indexed = true;
                    break;
                }
            }
            //为新表的字段存储到dm中
            tb.fields.add(Field.createField(tb, xid, fieldName, fieldType, indexed));
        }
        return tb.persistSelf(xid);
    }

private Table persistSelf(long xid) throws Exception {
        byte[] nameRaw = Parser.string2Byte(name);
        byte[] nextRaw = Parser.long2Byte(nextUid);
        byte[] fieldRaw = new byte[0];
        for(Field field : fields) {
            fieldRaw = Bytes.concat(fieldRaw, Parser.long2Byte(field.uid));
        }
        uid = ((TableManagerImpl)tbm).vm.insert(xid, Bytes.concat(nameRaw, nextRaw, fieldRaw));
        return this;
    }

Update操作

其步骤为

1 确认要更新的是哪张表

2 找出命中字段中满足where条件的uid,解析出存储的一条数据(表中可能有多个字段,以键值对存储)

3 找到要更新的字段fd

4 删除原有的字段,实则是设置xmax

5 更新一条数据,往DM插入更新后的值得到uid

6 在所有indexed字段建立的B+树中插入新的的key|uid

public int update(long xid, Update update) throws Exception {
        //找出命中字段中满足where条件的uid,如a=2,a=3
        List<Long> uids = parseWhere(update.where);
        Field fd = null;
        //找到要更新的字段fd
        for (Field f : fields) {
            if(f.fieldName.equals(update.fieldName)) {
                fd = f;
                break;
            }
        }
        if(fd == null) {
            throw Error.FieldNotFoundException;
        }
        //把需要更新的值变成相应的类型
        Object value = fd.string2Value(update.value);
        int count = 0;

        for (Long uid : uids) {
            //根据条件找到的uid
            byte[] raw = ((TableManagerImpl)tbm).vm.read(xid, uid);
            if(raw == null) continue;
            //更新即先删除再插入,删除时要判断死锁和可视化,实际上是设置xmax
            ((TableManagerImpl)tbm).vm.delete(xid, uid);
            //返回未更新之前的字段entry={name=xiaohong, age=18}
            Map<String, Object> entry = parseEntry(raw);
            //替换新值
            entry.put(fd.fieldName, value);//name=ZYJ,age=18
            //把新的值concat到一起
            raw = entry2Raw(entry);
            long uuid = ((TableManagerImpl)tbm).vm.insert(xid, raw);
            count ++;
            //更新两颗B+树
            for (Field field : fields) {
                if(field.isIndexed()) {
                    //字段的值,存储字段的值的uid
                    field.insert(entry.get(field.fieldName), uuid);
                }
            }
        }
        return count;
    }

insert操作

把插入的值合并到一个raw里面作为索引进行插入,然后为所有indexed字段建立的B+树更新树

public void insert(long xid, Insert insert) throws Exception {
        Map<String, Object> entry = string2Entry(insert.values);
        System.out.println("要插入的数据是"+entry);
        //把entry里面所有的值合并在一个raw里面
        byte[] raw = entry2Raw(entry);
        long uid = ((TableManagerImpl)tbm).vm.insert(xid, raw);
        for (Field field : fields) {
            if(field.isIndexed()) {
                field.insert(entry.get(field.fieldName), uid);
            }
        }
    }

read操作

同样,IM模块找出所有符合条件的uid,再利用VM模块进行可视化判断。

public String read(long xid, Select read) throws Exception {
        List<Long> uids = parseWhere(read.where);
        StringBuilder sb = new StringBuilder();
        for (Long uid : uids) {
            byte[] raw = ((TableManagerImpl)tbm).vm.read(xid, uid);
            if(raw == null) continue;
            Map<String, Object> entry = parseEntry(raw);
            sb.append(printEntry(entry)).append("\n");
        }
        return sb.toString();
    }

计算 Where 条件的范围

对表和字段的操作,有一个很重要的步骤,就是计算 Where 条件的范围, 比如Delete和Select都需要计算 Where,最终就需要获取到条件范围内所有的 UID,这里只支持了带有索引的两个条件的查询

//解析where语句,返回uid
private List<Long> parseWhere(Where where) throws Exception {
        long l0=0, r0=0, l1=0, r1=0;
        boolean single = false;//or字段连起来是false,其他是true
        Field fd = null;
        if(where == null) {//没有指定where范围
            for (Field field : fields) {
                if(field.isIndexed()) {
                    fd = field;
                    break;
                }
            }
            l0 = 0;
            r0 = Long.MAX_VALUE;
            single = true;
        } else {//指定了where范围
            for (Field field : fields) {
                //指定了是查找哪个字段
                if(field.fieldName.equals(where.singleExp1.field)) {
                    if(!field.isIndexed()) {
                        throw Error.FieldNotIndexedException;
                    }
                    fd = field;
                    break;
                }
            }
            if(fd == null) {
                throw Error.FieldNotFoundException;
            }
            CalWhereRes res = calWhere(fd, where);
            l0 = res.l0; r0 = res.r0;//第一个条件的低水位和高水位
            l1 = res.l1; r1 = res.r1;//第二个条件的低水位和高水位
            single = res.single;
        }
        List<Long> uids = fd.search(l0, r0);
        if(!single) {//or字段,增加后一个条件的uid
            List<Long> tmp = fd.search(l1, r1);
            uids.addAll(tmp);
        }
        return uids;
    }

在字段中搜寻满足where条件的高低水位如下,如果是and字段,取两个条件的高低水位的并集。

private CalWhereRes calWhere(Field fd, Where where) throws Exception {
        CalWhereRes res = new CalWhereRes();
        switch(where.logicOp) {
            case "":
                res.single = true;
                FieldCalRes r = fd.calExp(where.singleExp1);
                res.l0 = r.left; res.r0 = r.right;
                break;
            case "or":
                res.single = false;
                r = fd.calExp(where.singleExp1);
                res.l0 = r.left; res.r0 = r.right;
                r = fd.calExp(where.singleExp2);
                res.l1 = r.left; res.r1 = r.right;
                break;
            case "and"://两个条件的高低水位取交集
                res.single = true;
                r = fd.calExp(where.singleExp1);
                res.l0 = r.left; res.r0 = r.right;
                r = fd.calExp(where.singleExp2);
                res.l1 = r.left; res.r1 = r.right;
                if(res.l1 > res.l0) res.l0 = res.l1;//取left的最大值
                if(res.r1 < res.r0) res.r0 = res.r1;//取right的最小值
                break;
            default:
                throw Error.InvalidLogOpException;
        }
        return res;
    }

从一个where条件中解析出高低水位如下

//a>5  left=6 right=MAX_VALUE
//a<5  left=0 right=4;
//a=5  left=right=5
    public FieldCalRes calExp(SingleExpression exp) throws Exception {
        Object v = null;
        FieldCalRes res = new FieldCalRes();
        switch(exp.compareOp) {
            case "<":
                res.left = 0;
                v = string2Value(exp.value);
                res.right = value2Uid(v);
                if(res.right > 0) {
                    res.right --;
                }
                break;
            case "=":
                v = string2Value(exp.value);
                res.left = value2Uid(v);
                res.right = res.left;
                break;
            case ">":
                res.right = Long.MAX_VALUE;
                v = string2Value(exp.value);
                res.left = value2Uid(v) + 1;
                break;
        }
        return res;
    }

表的启动管理

由于 TBM 的表管理,使用的是链表串起的 Table 结构,所以就必须保存一个链表的头节点,即第一个表的 UID,这样在数据库 启动时,才能快速找到表信息。

 

数据库使用 Booter 类和 bt 文件,来管理 数据库 的启动信息,虽然现在所需的启动信息,只有一个:头表的 UID。Booter 类对外提供了两个方法:load 和 update,并保证了其原子性。update 在修改 bt 文件内容时,没有直接对 bt 文件进行修改,而是首先将内容写入一个 bt_tmp 文件中,随后将这个文件重命名为 bt 文件。以期通过操作系统重命名文件的原子性,来保证操作的原子性。

public void update(byte[] data) {
        //创建一个bt_tmp文件
        File tmp = new File(path + BOOTER_TMP_SUFFIX);
        try {
            tmp.createNewFile();
        } catch (Exception e) {
            Panic.panic(e);
        }
        if(!tmp.canRead() || !tmp.canWrite()) {
            Panic.panic(Error.FileCannotRWException);
        }
        //将启动信息写入一个 bt_tmp 文件中
        try(FileOutputStream out = new FileOutputStream(tmp)) {
            out.write(data);
            out.flush();
        } catch(IOException e) {
            Panic.panic(e);
        }
        try {
            //随后将这个文件重命名为 bt 文件,并覆盖之前的同名文件
            Files.move(tmp.toPath(), new File(path+BOOTER_SUFFIX).toPath(), StandardCopyOption.REPLACE_EXISTING);
        } catch(IOException e) {
            Panic.panic(e);
        }
        file = new File(path+BOOTER_SUFFIX);
        if(!file.canRead() || !file.canWrite()) {
            Panic.panic(Error.FileCannotRWException);
        }
    }

当创建tbm对象时,需要把数据库中所有的表加入到缓存中。

TableManagerImpl(VersionManager vm, DataManager dm, Booter booter) {
        this.vm = vm;
        this.dm = dm;
        this.booter = booter;
        this.tableCache = new HashMap<>();
        this.xidTableCache = new HashMap<>();
        lock = new ReentrantLock();
        loadTables();
    }


private void loadTables() {
        long uid = firstTableUid();
        while(uid != 0) {
            Table tb = Table.loadTable(this, uid);
            uid = tb.nextUid;
            tableCache.put(tb.name, tb);
        }
    }

TableManager的实现

TBM 层对外提供服务的是 TableManager 接口,如下:

public interface TableManager {
    BeginRes begin(Begin begin);
    byte[] commit(long xid) throws Exception;
    byte[] abort(long xid);

    byte[] show(long xid);
    byte[] create(long xid, Create create) throws Exception;

    byte[] insert(long xid, Insert insert) throws Exception;
    byte[] read(long xid, Select select) throws Exception;
    byte[] update(long xid, Update update) throws Exception;
    byte[] delete(long xid, Delete delete) throws Exception;
}

由于 TableManager 已经是直接被最外层 Server 调用),这些方法直接返回执行的结果,例如错误信息或者结果信息的字节数组)。

各个方法的具体实现很简单,不再赘述,无非是调用 VM 的相关方法。唯一值得注意的一个小点是,在创建新表时,采用的是头插法,所以每次创建表都需要更新 Booter 文件。

测试代码

public class test {
    public static void main(String[] args) throws Exception {
        TransactionManagerImpl tm= TransactionManager.create("cun/tm");
        DataManager dm=DataManager.create("cun/dm",1 << 20,tm);
        VersionManager vm=VersionManager.newVersionManager(tm,dm);
        TableManager tbm=TableManager.create("cun/",vm,dm);
        //开启事务
        BeginRes br=tbm.begin(new Begin());
        long xid=br.xid;
        //建立一张新表
        String ss="create table students " +
                "name string,age int32 " +
                "(index name age)";
        byte b[]=ss.getBytes(StandardCharsets.UTF_8);
        Object stat = Parser.Parse(b);
        tbm.create(xid,(Create) stat);
        System.out.println("===测试插入操作===");
        ss="insert into students values xiaohong 18";
        b=ss.getBytes(StandardCharsets.UTF_8);
        stat = Parser.Parse(b);
        tbm.insert(xid,(Insert) stat);
        System.out.println("===测试更新操作===");
        ss="update students set name = \"ZYJ\" where name = xiaohong";
        b=ss.getBytes(StandardCharsets.UTF_8);
        stat = Parser.Parse(b);
        tbm.update(xid,(Update) stat);
        System.out.println("===测试查询操作===");
        ss="select name,age from students where age=18";
        b=ss.getBytes(StandardCharsets.UTF_8);
        stat = Parser.Parse(b);
        byte[] output=tbm.read(xid,(Select) stat);
        System.out.println(new String(output));
        System.out.println("===测试删除操作===");
        ss=" delete from students where age =18";
        b=ss.getBytes(StandardCharsets.UTF_8);
        stat = Parser.Parse(b);
        tbm.delete(xid,(Delete) stat);
    }
}

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值