# AvenirSQL **Repository Path**: onlyyyy/AvenirSQL ## Basic Information - **Project Name**: AvenirSQL - **Description**: 用Node.js设计一个数据库 - **Primary Language**: NodeJS - **License**: GPL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 80 - **Forks**: 13 - **Created**: 2020-12-01 - **Last Updated**: 2025-01-02 ## Categories & Tags **Categories**: database-service **Tags**: None ## README # AvenirSQL ## 介绍 用Node.js设计一个数据库,支持常见的SQL语句 ## 安装使用 `git clone https://gitee.com/onlyyyy/AvenirSQL` `cd AvenirSQL` `npm i pm2 -g` 安装pm2管理工具 `pm2 start AvenirSQL` 注意查看run.ini 设置数据库的配置文件 更新配置需要重启数据库 `pm2 restart AvenirSQL` 建议使用高版本Node.js(v14+),通过版本管理工具n进行更新: **目前n只支持Mac和Linux** ```c npm i n -g n lts//下载最新版Nodejs ``` ## 技术特点 - 1.支持增删改查 - 2.精确查找支持哈希索引,范围查找支持B+树索引 - 3.智能缓存,提升QPS性能 - 4.提供用户管理,cli程序(curl.js) - 5.实现串行锁功能 - 6.灵活的策略配置 ## 代码结构 ./database/ : AvenirSQL核心实现 ./AvenirSQL.js : AvenirSQL启动程序 ./curl.js : cli程序 ./dtest.js : 测试程序 ./jmeter/ : Jmeter测试用例文件 ./db/ : 数据库和表数据 ./run.ini : 数据库配置文件 ./AvenirSQL/ : Nodejs版的AvenirSQL操作库 以下是项目中使用的Avenir开源组织开发的Node模块: 1. Nodejs通用方法库 [libcu](https://gitee.com/onlyyyy/libcu) 2. Avenir日志库 [avenir-log](https://gitee.com/onlyyyy/AvenirLog) 3. Nodejs对文件某一行的操作库 [nth-file](https://gitee.com/onlyyyy/nth-file) 4. Nodejs多叉树和字典树实现 [multiple-tree](https://gitee.com/onlyyyy_admin/multiple-tree) ## 支持的数据类型 1. number 数字,默认长度10 2. int 其实也是数字 并不是整数,默认长度10 3. bignumber 不限制长度的数字 4. varchar 字符型 默认长度20 5. string 字符型 默认长度20 6. bigstring 不限制长度的字符 ## 具体技术实现 - 1.统一的底层错误处理函数,避免函数重复传递参数。 ```js async response(type, client) { let res = null; if (typeof type == 'string') { res = getError(type); if (!res) { res = unknown; } } else { let code = type.code; let data = type.data; res = getError(code); if (!res) { res = unknown; } res.data = data; } client.write(JSON.stringify(res)); //如果没有配置默认短连接 if( ini.db.keepAlive != true) { toLog("主动踢掉客户端的连接"); client.end(); } } ``` 2.数据库结构 数据库:文件夹名 表:数据文件、哈希索引文件、B+树索引文件(聚合索引) - 数据文件: 第一行存放表结构定义,第二行开始第一位为压缩的16进制数,表示该行元素是否为空,后续存储按分隔符排列。 - 哈希索引: 对象,key为主键,value为所在文件的行号 - B+树索引: 存放B+树的结构 3.连接管理 为了区分不同的用户对数据库进行的不同操作,如同一秒内多个进程进行多次请求,AvenirSQL会生成一个签名,用户登录后需使用此签名进行操作。 4.串行锁 进行操作前加锁,操作完成后解锁,并刷新缓存(select语句不会刷新缓存) ```js //自动释放锁防止数据库死锁 async releaseLock() { let now = moment().valueOf(); let releaseLockTime = ini.db.releaseLockTime; releaseLockTime = releaseLockTime > ini.db.checkLockTime ? releaseLockTime : ini.db.checkLockTime; for(let key in this.table) { let tables = this.table[key]; for(let subKey in tables) { let times = tables[subKey]; if(moment(now).diff(moment(times),'seconds') > releaseLockTime) { delete tables[subKey]; toLog("自动释放了锁 ",tables[subKey]); } } } } ``` 5.缓存 目前共五类缓存,数据库配置文件缓存和表结构缓存不会刷新,哈希索引、表数据、B+树索引缓存会定时刷新。 6.解析SQL 在此感谢阿里巴巴的sql解析器 [node-sqlparser](https://www.npmjs.com/package/node-sqlparser) AvenirSQL独有的sql会先解析,除此之外的SQL会转交给node-sqlparser。 ```js //包含原生SQL和能够被AvenirSQL识别的语句 async parse(sql, sign) { //先解析AvenirSQL特有的语句 再解析原生SQL toLog("要解析的 sql为 ", sql); let raw = this.getArray(sql); if (raw.length === 0 || !sql) { throw ('SQL_PARSE_ERROR'); } else { //AvenirSQL解析出错不报错,转给解析器解析,解析器报错直接throw try { await this.parseAvenirSql(raw, sql, sign); } catch (error) { //不是内部定义的错误就代表程序处理出错了 toLog('error = ', error); if (error == SUCCESS || error != 'error') { throw (error); } //不需要try catch了,底层会抓住错误 let par = this.parseSql(sql); await this.doSql(par, sign); } } } ``` 7.事务 事务操作在缓存中临时处理,使用串行锁避免并发,不会出现重复读,读未提交等。 rollback操作直接清除缓存,commit操作提交 写文件,释放锁,同样会释放缓存。 超时自动释放锁 避免出现数据库死锁的情况。 8.日志 暂时只支持记录类日志,未实现undolog,binlog等日志,通过Trace类,avenir-log模块实现。 9.cli程序 提供一个与AvenirSQL交互的工具,实现自动重连功能,当发现报错为code 2 签名失效则直接重连。 ```js //检查返回值,超时就自动重发 async function checkError(response) { if (response && response.code == 2) { //返回值是2代表签名失效 重新登录即可 let data = await safeConnect(); if (data.code === 0) { data = data.data; } else { //说明重新登录也报错了 数据库故障 return { code: -1, data: null, } } return { code: 1, data }; } else if (response.code == 0) { return { code: 0, data: null }; } else { return { code: -1, data: null, }; //其他错误 其实返回这个没啥用 只需要判断1 就行 } } ``` 10. select distinct 在返回了结果集之后,根据distinct参数来对数据去重,通过依次插入到多叉树中,一旦发现插入失败则表示数据重复了。 ```js //处理distinct的函数 20210222 考虑用多叉树来处理 async doDistinct(data, columns, tableDetail) { let mulTree = new MultipleTree(); if (columns.length == 1 && columns[0].expr.column == tableDetail.key) { //如果是主键的话本身就是distinct了 toLog("优化器跳过distinct"); return data; } //遍历结果集 剔除重复的列 for (let i = 0; i < data.length; i++) { let line = data[i]; if(mulTree.insert(line) === false) { data.splice(i,1); i--; } else { continue; } } return data; } ``` 11. 导入导出 导出即执行select操作,并将SQL语句转换为insert语句。 导入则按分隔符执行SQL语句 导出文件名规范:dump[table || database]_dbname_tableName_YYYYMMDDHHmmss.sql ## 接口规范 数据库连接使用tcp通信,传输文本为JSON数据格式。 1. 登录 ```js { user:"root", password:"123456" type:"login" } ``` 返回值: ```js { code:0, message:"success", data:"ef3d843f26c4e900e9ab4979f324d5571a4cb5f5c011278b36985b2802c828185ad0bdd7e19390cfffe479afe1b09d1c" } ``` 2. 数据库SQL操作 ```js { sign:"ef3d843f26c4e900e9ab4979f324d5571a4cb5f5c011278b36985b2802c828185ad0bdd7e19390cfffe479afe1b09d1c", type:"sql", data:"select * from test" } ``` 返回值: ```js { code : 0, message:"success", data:[{ name:"test", id:"1", }] } ``` 3. 数据库事务 ```js { sign:"ef3d843f26c4e900e9ab4979f324d5571a4cb5f5c011278b36985b2802c828185ad0bdd7e19390cfffe479afe1b09d1c", id:"transID" //事务执行流程: begin->sql->commit,begin的时候会给id 后续用id来执行sql type:"trans", data:"delete from test", } ``` 返回值: ```js { code : 0, message:"success", } ``` ### 行数据分隔符 分隔符为∫,故所有的列数据不可以含有∫符号,否则会报错 ## 错误代码表 ```js { //不该发生的错误 UNKNOWN_CMD: { code: -1, message: 'unknown command', }, SYSTEM_BUSY: { code: -2, message: 'system busy', }, BAD_REQUEST: { code: -3, message: 'bad request', //请求格式无法JSON序列化 }, UNKNOWN_ERROR: { code: -100, message: 'unknown error' }, //成功 AVENIR_SUCCESS: { code: 0, message: 'success' }, //程序级别错误 SQL_PARSE_ERROR: { code: 1, message: 'sql parse error' }, NOT_CONNECTED: { code: 2, message: "no connect info, please login first" }, //大意了没有3 4不吉利 年轻人不讲5的 FILE_NOT_EXIST: { code: 6, message: 'file not exist' }, GEN_SIGN_ERROR: { code: 7, message: 'generate sign error', }, SET_SIGN_EXIT: { code: 8, message: 'sign exit', }, LACK_OF_SIGN: { code: 9, message: 'lack of sign, please login first', }, INVALID_NAME: { code: 10, message: "invalid name", }, BAD_USER: { code: 11, message: 'user or password error', }, PERMISSION_DENIED: { code: 12, message: 'permission denied', }, //以下是数据库类错误 DATABASE_NOT_FOUND: { code: 1001, message: 'database not found' }, DATABASE_EXIST: { code: 1002, message: 'database already exist', }, TABLE_NOT_FOUND: { code: 1003, message: 'table not found' }, TABLE_EXIST: { code: 1004, message: 'table already exist' }, INVALID_SQL_ERROR: { code: 1005, message: "invalid sql or sql parse Error" }, TOO_MANY_COLUMNS: { code: 1006, message: 'too many columns', }, COLUMN_NOT_FOUND: { code: 1007, message: 'column not found', }, COLUMN_NOT_MATCH: { code: 1008, message: 'columns and values not match', }, COLUMN_REPEAT: { code: 1009, message: 'columns repeat', }, COLUMN_NOT_NULL: { code: 1010, message: 'some columns cant be null', }, COLUMN_NOT_CHECK: { code: 1011, message: 'column not check error', }, ONLY_ONE_KEY: { code: 1012, message: 'AvenirSQL only support one key', }, LACK_OF_PRIMARY_KEY: { code: 1013, message: 'lack of primary key', }, KEY_EXIST: { code: 1014, message: 'duplicate primary key value', }, OPER_NO_ROW: { code: 1015, message: 'the operated row not found, may be not a error' }, VALUE_NOT_NUMBER: { code: 1016, message: 'compared value is not a number', }, SQL_TOO_LONG: { code: 1017, message: 'sql is too long', }, SQL_NOT_SUPPORT: { code: 1018, message: 'AvenirSQL dont support this sql yet', }, GET_LOCK_FAILED: { code: 1019, message: 'AvenirSQL get lock timeout', }, RELEASE_LOCK_FAILED: { code: 1020, message: 'AvenirSQL release lock failed', }, USER_EXISTS: { code: 1021, message: 'user already exists', }, USER_NOT_FOUND: { code: 1022, message: 'bad user or password', }, //事务类错误 TRANS_NOT_FOUND: { code: 1023, message: 'invalid trans id', }, TRANS_TIME_OUT: { code: 1024, message: 'trans timeout', }, NO_GROUP_DIS: { code: 1025, message: 'AvenirSQL dont support group by yet' }, COUNT_NO_TABLE: { code: 1026, message: `count(colums) cant be [table.column]` }, WHITE_SPACE_ERROR: { code: 1027, message: `sql cant contain AvenirSQL's separator:${this.WHITE_SPACE}`, }, NOT_SUPPORT_DATA: { code: 1028, message: 'AvenirSQL dont support such data type', }, COLUMN_TYPE_ERROR: { code: 1029, message: 'table columns type error(number or string)', }, COLUMN_OUT_OF_LENGTH: { code: 1030, message: 'columns over the length', }, DUMP_TABLENAME_ERROR: { code: 1031, message: 'dumped table name error' }, DUMP_SQL_ERROR: { code: 1032, message: "dump sql error" }, } ``` ## SQL规范 1. 暂不支持join、 like、 group by 2. show命令 `show database` 展示目前所有的数据库列表 `show table [dbname]` 展示该数据库下的所有表名,不输入dbname会去获取默认值 `show dump` 展示默认的导出目录下所有的文件名 ```json {"code":0,"message":"success","data":["home_def","hot","t"]} ``` 3. update 不支持不带where条件的全表更新 如`update t set a = '1'` 4. distinct distinc a,b,c 即代表数据库里面a,b,c三个列都重复才会判断重复。 通过多叉树模块multiple-tree来实现 5. dump dump table dbname.tableName as select * from tableName where a > 10000 如果不带SQL语句,则导出全表,在配置文件配置是否是覆盖导出和导入 ## 配置文件示例 文件名必须是run.ini,在安装目录下 若找不到此文件,AvenirSQL会自动创建默认的配置文件,内容如下: 6. exec `exec [fileName]` 执行SQL文件 若只有文件名,则会拼上ini文件的dump.path,否则就直接读取全路径。 ```ini [main] ip=127.0.0.1 port=44944 #数据库的工作目录 path=./db #是否输出日志至显示屏 频繁I/O操作将影响性能 ifConsoleLog=true [db] #maxConnect=100 目前存在问题 #签名的有效期(秒) signValidTime=100 #事务自动回滚时间(秒) rollbackTime=10 #是否维持长连接 keepAlive=false #是否记录debug类的日志 debug=false #每个连接的超时时间 毫秒 timeOut=10000 #数据库表列的上限 maxColoums=100 #数据库SQL的最大长度 maxSqlLength=200 #默认的用户数据库 user=User #缓存失效的时间(秒) cacheInvalid=200 #检查缓存失效的频率(秒) clearCache=500 #updateCache 缓存更新策略 0-增删改之后会清除缓存 但直到下次操作之后才会重新读取缓存 1-立即读取缓存 updateCache=0 #数据库得到锁的超时时间(秒) lockTimeOut=3 #数据库得不到锁的重复尝试次数(次) 默认不得小于10 不得大于100 lockTryTime=10 #自动释放锁的时间 防止数据库出现死锁 单位(秒) 范围为3-100 releaseLockTime=10 #轮询判断是否释放锁的周期 (秒) 默认不会大于releaseLockTime checkLockTime=10 #文件名配置 [name] #数据库名为新建的目录 不需要另外写文件了 #数据库表文件后缀 table=.table #数据库B+索引文件后缀 index=.bpx #数据库哈希索引文件后缀 hash=.hash #整体的配置文件 rootSet=AvenirSQL.json #每个数据库的配置文件 dbSet=.json #AvenirSQL管理程序的配置 [curl] #数据库导出功能的配置 [dump] path=./dump #是否先删表再导入表 默认是先删除再导入 notForce=false #日志系统配置 [log] #日志输出目录 logPath=./log #写入日志的周期(秒) loopTime=10 #日志文件名 logName=AvenirSQL ``` ## 总结 技术没有高低贵贱之分,脑海中如果有想法的话,我们要做的就是去把它实现。 编程之路漫漫修远兮,吾将上下而求索。 谢谢。