手写数据库轮子项目 MYDB 之九 | TableManager (TBM) 字段解析与表管理

TBM,即表管理器的实现。TBM 实现了对字段结构和表结构的管理。

一、SQL 解析器

Parser 实现了对类 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

1. 语句切割

Tokenizer 类对语句进行逐字节解析,根据空白符或者上述词法规则,将语句切割成多个 token。

    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;
        }
    }

2. 语句解析

解析过程是根据解析出的第一个 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;

                /// ...

                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;
    }

二、字段管理

1. 存储结构

这里的字段与表管理,不是管理各个条目中不同的字段的数值等信息,而是管理表和字段的结构数据,例如表名、表字段信息和字段索引等。

由于 TBM 基于 VM,单个字段信息和表信息都是直接保存在 Entry 中。

字段的二进制表示如下:

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

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

[StringLength][StringData]

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

2. 读取字段

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

    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;
    }

3. 创建字段

创建一个字段的方法类似,将相关的信息通过 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));
}

三、表管理

1. 存储结构

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

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

这里由于每个 Entry 中的数据,字节数是确定的,于是无需保存字段的个数。

2. 读取表

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

    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);
    }

3. 解析表

由于 uid 的字节数是固定的(8个),所以无需保存字段的个数,循环读取即可。

    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;
        /// 循环读取并添加 field 字段
        while(position < raw.length) {
            long uid = Parser.parseLong(Arrays.copyOfRange(raw, position, position+8));
            position += 8;
            fields.add(Field.loadField(this, uid));
        }
        return this;
    }

4. 创建表

public class Create {
    public String tableName;//表名
    public String[] fieldName;//存储的字段名
    public String[] fieldType;//存储的字段类型
    public String[] index;//存储的索引
}
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;
                }
            }
            tb.fields.add(Field.createField(tb, xid, fieldName, fieldType, indexed));
        }

        return tb.persistSelf(xid);
    }

5. 更新数据

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;
    }

6. 插入数据

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);
            }
        }
    }

7. 读数据

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();
    }

8. 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);
        }
    }

 创建新表时,采用的是头插法,所以每次创建表都需要更新 Booter 文件。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值