1
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"minSdkVersion": "21",
|
||||
"dependencies":[]
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
import xSqliteHelp from 'uts.sdk.modules.utsXsqliteS.xSqliteHelp';
|
||||
import SQLiteResultByKt from 'uts.sdk.modules.utsXsqliteS.SQLiteResultByKt';
|
||||
import SQLiteConfigByKt from 'uts.sdk.modules.utsXsqliteS.SQLiteConfigByKt';
|
||||
import SQLiteExecuteBatchParamsByKt from 'uts.sdk.modules.utsXsqliteS.SQLiteExecuteBatchParamsByKt';
|
||||
import {SQLiteConfig,SQLiteResult,SQLiteExecuteBatchParams} from '../interface.uts';
|
||||
import Kotlin from 'kotlin.jvm.internal.Intrinsics.Kotlin';
|
||||
|
||||
|
||||
/**
|
||||
* SQLite数据库操作类
|
||||
* 提供SQLite数据库的创建、查询、更新等基本操作,支持加密和事务处理
|
||||
*/
|
||||
export class xSqlite {
|
||||
/** 数据库配置信息 */
|
||||
config : SQLiteConfig;
|
||||
private db : xSqliteHelp|null = null;
|
||||
|
||||
constructor(config : SQLiteConfig = {}) {
|
||||
let realCofing:SQLiteConfig = config;
|
||||
let directory:null|string = realCofing?.defaultDirectory??null
|
||||
if(typeof directory == 'string' && directory !=null){
|
||||
directory = UTSAndroid.convert2AbsFullPath(directory)
|
||||
}
|
||||
realCofing.defaultDirectory = directory
|
||||
this.config = realCofing;
|
||||
this.db = new xSqliteHelp(
|
||||
UTSAndroid.getAppContext()!,
|
||||
SQLiteConfigByKt(
|
||||
password = this.config.password,
|
||||
encryption = (this.config?.encryption??false),
|
||||
defaultDirectory = (this.config?.defaultDirectory??null),
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
private converFormat(result?:SQLiteResultByKt):SQLiteResult{
|
||||
if(result==null){
|
||||
return {
|
||||
rows: [] ,
|
||||
columns: [] ,
|
||||
changes: null,
|
||||
lastInsertRowid: null,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
let rows = [] as any[][];
|
||||
let maps = [] as Map<string,any>[];
|
||||
let columns = [] as string[];
|
||||
if(result?.rows!=null){
|
||||
let cols = result!.columns as ArrayList<string>;
|
||||
columns = Array.fromNative(cols)
|
||||
}
|
||||
if(result?.rows!=null){
|
||||
let crows = result!.rows as ArrayList<ArrayList<any>>;
|
||||
for (col in crows) {
|
||||
rows.push(Array.fromNative(col))
|
||||
}
|
||||
}
|
||||
if(result?.maps!=null){
|
||||
let cmaps = result!.maps as ArrayList<LinkedHashMap<string,any>>;
|
||||
for (col in cmaps) {
|
||||
let item = col as LinkedHashMap<string,any>;
|
||||
let newmap = new Map<string,any>()
|
||||
for(key in columns){
|
||||
newmap.set(key,item.get(key)!)
|
||||
}
|
||||
maps.push(newmap)
|
||||
}
|
||||
}
|
||||
return {
|
||||
rows: rows,
|
||||
columns: columns,
|
||||
maps: maps,
|
||||
changes: result?.changes,
|
||||
lastInsertRowid: result?.lastInsertRowid,
|
||||
error: result?.error,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建数据库
|
||||
* @param fileName 数据库文件名,如果为null则使用默认名称'sqlite'
|
||||
* @returns 创建成功返回true,失败返回false
|
||||
*/
|
||||
createDb(fileName:string|null = null) : boolean{
|
||||
if (this.db==null){
|
||||
return false;
|
||||
}
|
||||
const result = this.db!.createDb(fileName)
|
||||
|
||||
return result != null;
|
||||
}
|
||||
/**
|
||||
* 执行SQL语句
|
||||
* @param sql SQL语句
|
||||
* @param params SQL参数数组
|
||||
* @returns SQLiteResult 执行结果,包含changes(影响行数)和lastInsertRowid(最后插入行ID)
|
||||
*/
|
||||
run(sql: string, params: any[] = []): SQLiteResult {
|
||||
try {
|
||||
if (this.db==null){
|
||||
return {error:'Database not initialized'};
|
||||
}
|
||||
const result = this.db!.run(sql, params);
|
||||
return this.converFormat(result)
|
||||
} catch (error) {
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询数据
|
||||
* @param sql 查询SQL语句
|
||||
* @param params 查询参数数组
|
||||
* @returns SQLiteResult 查询结果,包含rows(数据行)和columns(列名)
|
||||
*/
|
||||
query(sql: string, params: any[] = [] as any[]): SQLiteResult {
|
||||
if (this.db==null){
|
||||
return {error:'Database not initialized'};
|
||||
}
|
||||
|
||||
const result = this.db!.query(sql, params.toKotlinList());
|
||||
|
||||
return this.converFormat(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入数据
|
||||
* @param table 表名
|
||||
* @param data 要插入的数据对象
|
||||
* @returns SQLiteResult 插入结果
|
||||
*/
|
||||
insert(table: string, data: UTSJSONObject): SQLiteResult {
|
||||
if (this.db==null){
|
||||
return {error:'Database not initialized'};
|
||||
}
|
||||
const result = this.db!.insert(table, new org.json.JSONObject(data.toJSONString()));
|
||||
return this.converFormat(result)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 更新数据
|
||||
* @param table 表名
|
||||
* @param data 要更新的数据对象
|
||||
* @param where WHERE条件语句
|
||||
* @param params WHERE条件参数数组
|
||||
* @returns SQLiteResult 更新结果
|
||||
*/
|
||||
update(table: string, data: UTSJSONObject, where: string, params: any[] = []): SQLiteResult {
|
||||
if (this.db==null){
|
||||
return {error:'Database not initialized'};
|
||||
}
|
||||
const result = this.db!.update(table,
|
||||
new org.json.JSONObject(data.toJSONString()),
|
||||
where,
|
||||
params.toKotlinList()
|
||||
);
|
||||
return this.converFormat(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据
|
||||
* @param table 表名
|
||||
* @param where WHERE条件语句
|
||||
* @param params WHERE条件参数数组
|
||||
* @returns SQLiteResult 删除结果
|
||||
*/
|
||||
delete(table: string, where: string, params: any[] = []): SQLiteResult {
|
||||
if (this.db==null){
|
||||
return {error:'Database not initialized'};
|
||||
}
|
||||
const result = this.db!.delete(table, where ,params.toKotlinList());
|
||||
return this.converFormat(result)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 检查表是否存在
|
||||
* @param tableName 表名
|
||||
* @returns 表存在返回true,不存在返回false
|
||||
*/
|
||||
tableExists(tableName: string): boolean {
|
||||
if (this.db==null){
|
||||
return false
|
||||
}
|
||||
return this.db!.tableExists(tableName);
|
||||
}
|
||||
/**
|
||||
* 创建数据表
|
||||
* @param tableName 表名
|
||||
* @param columns 列定义对象,key为列名,value为列类型定义
|
||||
* @returns SQLiteResult 创建结果
|
||||
*/
|
||||
createTable(tableName: string, columns: UTSJSONObject): SQLiteResult {
|
||||
try {
|
||||
if (this.db==null){
|
||||
return {error:'Database not initialized'};
|
||||
}
|
||||
const dataMap = new Map<string,string>();
|
||||
for(const key in columns){
|
||||
const item = columns.getString(key)!;
|
||||
dataMap.set(key,item)
|
||||
}
|
||||
const result = this.db!.createTable(tableName, dataMap);
|
||||
return this.converFormat(result)
|
||||
} catch (error) {
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据表
|
||||
* @param tableName 要删除的表名
|
||||
* @returns SQLiteResult 删除结果
|
||||
*/
|
||||
dropTable(tableName: string): SQLiteResult {
|
||||
if (this.db==null){
|
||||
return {error:'Database not initialized'};
|
||||
}
|
||||
const result = this.db!.dropTable(tableName);
|
||||
return this.converFormat(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭数据库连接
|
||||
* 释放数据库资源,关闭后需要重新创建才能使用
|
||||
*/
|
||||
close(): void {
|
||||
if (this.db!=null) {
|
||||
this.db!.close();
|
||||
this.db = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始事务
|
||||
* 开始一个新的事务,在提交或回滚之前,所有操作都在事务内
|
||||
* @returns SQLiteResult 事务开始结果
|
||||
*/
|
||||
beginTransaction(): SQLiteResult{
|
||||
if (this.db==null){
|
||||
return {error:'Database not initialized'};
|
||||
}
|
||||
const result = this.db!.beginTransaction();
|
||||
return this.converFormat(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交事务
|
||||
* 提交当前事务的所有操作
|
||||
* @returns SQLiteResult 事务提交结果
|
||||
*/
|
||||
commit(): SQLiteResult {
|
||||
if (this.db==null){
|
||||
return {error:'Database not initialized'};
|
||||
}
|
||||
const result = this.db!.commit();
|
||||
return this.converFormat(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚事务
|
||||
* 撤销当前事务中的所有操作
|
||||
* @returns SQLiteResult 事务回滚结果
|
||||
*/
|
||||
rollback(): SQLiteResult {
|
||||
if (this.db==null){
|
||||
return {error:'Database not initialized'};
|
||||
}
|
||||
const result = this.db!.rollback();
|
||||
return this.converFormat(result)
|
||||
}
|
||||
/**
|
||||
* 批量执行SQL语句
|
||||
* @param statements SQL语句数组,每个元素包含sql和params
|
||||
* @returns SQLiteResult[] 每个语句的执行结果数组
|
||||
*/
|
||||
executeBatch(statements: SQLiteExecuteBatchParams[]): SQLiteResult[] {
|
||||
if (this.db==null){
|
||||
console.error('Database not initialized')
|
||||
return [];
|
||||
}
|
||||
const list = new ArrayList<SQLiteExecuteBatchParamsByKt>()
|
||||
for(const item in statements){
|
||||
list.add(SQLiteExecuteBatchParamsByKt(
|
||||
sql = item.sql,
|
||||
params = (item?.params??[]).toKotlinList()
|
||||
))
|
||||
}
|
||||
const result = this.db!.executeBatch(list);
|
||||
let resultList = [] as SQLiteResult[]
|
||||
for(const item in result){
|
||||
resultList.push(this.converFormat(item))
|
||||
}
|
||||
|
||||
return resultList
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存数据库到本地文件
|
||||
* @param filename 保存的文件名,可选,默认为'sqlite'
|
||||
* @returns SQLiteResult 保存结果
|
||||
*/
|
||||
saveLocal(filename?: string): SQLiteResult {
|
||||
if (this.db==null){
|
||||
return {error:'Database not initialized'};
|
||||
}
|
||||
const result = this.db!.saveLocal(filename);
|
||||
return this.converFormat(result)
|
||||
}
|
||||
/**
|
||||
* 从本地文件加载数据库
|
||||
* @param filename 要加载的数据库文件名
|
||||
* @returns SQLiteResult 加载结果
|
||||
*/
|
||||
loadLocal(filename:string): SQLiteResult{
|
||||
|
||||
const result = this.db!.loadLocal(filename);
|
||||
return this.converFormat(result)
|
||||
}
|
||||
/**
|
||||
* 获取数据库文件路径
|
||||
* 获取当前数据库文件的完整路径,获取前会备份到缓存目录
|
||||
* @returns string|null 数据库文件路径,如果数据库未初始化则返回null
|
||||
*/
|
||||
getDatabasePath():string|null{
|
||||
if (this.db==null){
|
||||
console.error('Database not initialized')
|
||||
return null
|
||||
}
|
||||
return this.db!.getDatabasePath();
|
||||
}
|
||||
/**
|
||||
* 当前数据库的目录
|
||||
*/
|
||||
setDefaultDirectory(directory: string){
|
||||
if (this.db==null){
|
||||
console.error('Database not initialized')
|
||||
return
|
||||
}
|
||||
let rdirectory = directory;
|
||||
if(rdirectory!=''){
|
||||
rdirectory = UTSAndroid.convert2AbsFullPath(directory)
|
||||
}
|
||||
this.db!.setDefaultDirectory(rdirectory);
|
||||
}
|
||||
/**
|
||||
* 设置数据库密码
|
||||
* 空值或者null即删除密码
|
||||
*/
|
||||
setPassword(password: string|null = null){
|
||||
if (this.db==null){
|
||||
console.error('Database not initialized')
|
||||
return
|
||||
}
|
||||
this.db!.setPassword(password);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
package uts.sdk.modules.utsXsqliteS
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import io.dcloud.uts.console
|
||||
|
||||
data class SQLiteConfigByKt(
|
||||
var password: String? = null,
|
||||
var encryption: Boolean = false,
|
||||
var locateFile: ((String) -> String)? = null,
|
||||
var defaultDirectory: String? = null
|
||||
)
|
||||
|
||||
data class SQLiteResultByKt(
|
||||
val rows: List<List<Any>>? = null,
|
||||
val columns: List<String>? = null,
|
||||
val changes: Int? = null,
|
||||
val lastInsertRowid: Long? = null,
|
||||
val error: String? = null,
|
||||
val maps: List<Map<String, Any>>? = null
|
||||
)
|
||||
|
||||
data class SQLiteExecuteBatchParamsByKt(
|
||||
val sql: String,
|
||||
val params: List<Any>? = null
|
||||
)
|
||||
|
||||
class xSqliteHelp(private val context: Context, private val config: SQLiteConfigByKt = SQLiteConfigByKt()) {
|
||||
private var db: SQLiteDatabase? = null
|
||||
private var isInTransaction: Boolean = false
|
||||
private var fileURLPath: String? = null
|
||||
|
||||
fun createDb(filename: String? = null): SQLiteDatabase? {
|
||||
val fname = filename ?: "sqlite"
|
||||
val dbFile = if (config.defaultDirectory != null) {
|
||||
File(config.defaultDirectory, "$fname.db")
|
||||
} else {
|
||||
context.getDatabasePath("$fname.db")
|
||||
}
|
||||
|
||||
try {
|
||||
if (!dbFile.parentFile?.exists()!!) {
|
||||
dbFile.parentFile?.mkdirs()
|
||||
}
|
||||
|
||||
db = SQLiteDatabase.openOrCreateDatabase(dbFile, null)
|
||||
if (config.encryption && config.password != null) {
|
||||
run("PRAGMA key = ?", listOf(config.password!!))
|
||||
}
|
||||
fileURLPath = dbFile.absolutePath
|
||||
return db
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 执行SQL查询
|
||||
fun run(sql: String, params: List<Any> = emptyList()): SQLiteResultByKt {
|
||||
return try {
|
||||
if (db == null) throw Exception("Database not initialized")
|
||||
val statement = db!!.compileStatement(sql)
|
||||
params.forEachIndexed { index, param ->
|
||||
when (param) {
|
||||
is String -> statement.bindString(index + 1, param)
|
||||
is Int -> statement.bindLong(index + 1, param.toLong())
|
||||
is Long -> statement.bindLong(index + 1, param)
|
||||
is Double -> statement.bindDouble(index + 1, param)
|
||||
is ByteArray -> statement.bindBlob(index + 1, param)
|
||||
null -> statement.bindNull(index + 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 根据SQL语句类型选择合适的执行方法
|
||||
val sqlUpperCase = sql.trim().uppercase()
|
||||
val result = if (sqlUpperCase.startsWith("INSERT")) {
|
||||
// 对于INSERT语句,使用executeInsert获取lastInsertRowid
|
||||
val lastId = statement.executeInsert()
|
||||
SQLiteResultByKt(changes = 1, lastInsertRowid = lastId)
|
||||
} else {
|
||||
// 对于UPDATE/DELETE/其他语句,使用executeUpdateDelete获取受影响的行数
|
||||
val changes = statement.executeUpdateDelete()
|
||||
SQLiteResultByKt(changes = changes, lastInsertRowid = null)
|
||||
}
|
||||
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
SQLiteResultByKt(error = e.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 查询数据
|
||||
fun query(sql: String, params: List<Any> = emptyList()): SQLiteResultByKt {
|
||||
return try {
|
||||
if (db == null) throw Exception("Database not initialized")
|
||||
val args = params.map { it?.toString() ?: "" }.toTypedArray()
|
||||
val cursor = db!!.rawQuery(sql, args)
|
||||
val columns = ArrayList(cursor.columnNames.toList())
|
||||
val rows = mutableListOf<List<Any>>()
|
||||
val maps = mutableListOf<Map<String, Any>>()
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val row = mutableListOf<Any>()
|
||||
val map = mutableMapOf<String, Any>()
|
||||
for (i in 0 until cursor.columnCount) {
|
||||
val columnName = cursor.getColumnName(i)
|
||||
val value = when (cursor.getType(i)) {
|
||||
android.database.Cursor.FIELD_TYPE_STRING -> cursor.getString(i)
|
||||
android.database.Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(i)
|
||||
android.database.Cursor.FIELD_TYPE_FLOAT -> cursor.getDouble(i)
|
||||
android.database.Cursor.FIELD_TYPE_BLOB -> cursor.getBlob(i)
|
||||
else -> ""
|
||||
}
|
||||
row.add(value)
|
||||
map[columnName] = value
|
||||
}
|
||||
rows.add(row)
|
||||
maps.add(map)
|
||||
}
|
||||
cursor.close()
|
||||
SQLiteResultByKt(rows = rows, columns = columns, maps = maps)
|
||||
} catch (e: Exception) {
|
||||
SQLiteResultByKt(error = e.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 插入数据
|
||||
fun insert(table: String, data: JSONObject): SQLiteResultByKt {
|
||||
val keys = mutableListOf<String>()
|
||||
val values = mutableListOf<Any>()
|
||||
data.keys().forEach { key ->
|
||||
keys.add(key)
|
||||
values.add(data.get(key))
|
||||
}
|
||||
val sql = "INSERT INTO $table (${keys.joinToString(",")}) VALUES (${keys.map { "?" }.joinToString(",")})"
|
||||
return run(sql, values)
|
||||
}
|
||||
|
||||
// 更新数据
|
||||
fun update(table: String, data: JSONObject, where: String, params: List<Any> = emptyList()): SQLiteResultByKt {
|
||||
val sets = mutableListOf<String>()
|
||||
val values = mutableListOf<Any>()
|
||||
data.keys().forEach { key ->
|
||||
sets.add("$key = ?")
|
||||
values.add(data.get(key))
|
||||
}
|
||||
values.addAll(params)
|
||||
val sql = "UPDATE $table SET ${sets.joinToString(",")} WHERE $where"
|
||||
return run(sql, values)
|
||||
}
|
||||
|
||||
// 删除数据
|
||||
fun delete(table: String, where: String, params: List<Any> = emptyList()): SQLiteResultByKt {
|
||||
val sql = "DELETE FROM $table WHERE $where"
|
||||
return run(sql, params)
|
||||
}
|
||||
|
||||
// 保存数据库到本地
|
||||
fun saveLocal(filename: String? = null): SQLiteResultByKt {
|
||||
return try {
|
||||
if (db == null) throw Exception("Database not initialized")
|
||||
val fname = filename ?: "sqlite"
|
||||
|
||||
// 获取当前数据库文件的实际路径
|
||||
val dbPath = db!!.path
|
||||
val dbFile = if (dbPath.isNotEmpty()) File(dbPath) else context.getDatabasePath("sqlite.db")
|
||||
// 检查源文件是否存在
|
||||
if (!dbFile.exists()) {
|
||||
return SQLiteResultByKt(error = "未能找到数据库文件")
|
||||
}
|
||||
|
||||
// 使用配置的默认目录或系统文件目录
|
||||
val targetDir = if (config.defaultDirectory != null) File(config.defaultDirectory) else context.filesDir
|
||||
val targetFile = File(targetDir, "$fname.db")
|
||||
// 确保目标目录存在
|
||||
targetFile.parentFile?.mkdirs()
|
||||
if(dbFile.absolutePath != targetFile.absolutePath){
|
||||
dbFile.copyTo(targetFile, overwrite = true)
|
||||
}
|
||||
|
||||
SQLiteResultByKt()
|
||||
} catch (e: Exception) {
|
||||
SQLiteResultByKt(error = e.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 从本地加载数据库
|
||||
fun loadLocal(filename: String): SQLiteResultByKt {
|
||||
return try {
|
||||
// 使用配置的默认目录或系统文件目录
|
||||
val sourceDir = if (config.defaultDirectory != null) File(config.defaultDirectory!!) else context.filesDir
|
||||
val sourceFile = File(sourceDir, "$filename.db")
|
||||
|
||||
if (!sourceFile.exists()) {
|
||||
return SQLiteResultByKt(error = "没有数据库")
|
||||
}
|
||||
val dbFile = context.getDatabasePath("sqlite.db")
|
||||
sourceFile.copyTo(dbFile, overwrite = true)
|
||||
createDb(filename)
|
||||
SQLiteResultByKt()
|
||||
} catch (e: Exception) {
|
||||
SQLiteResultByKt(error = "数据库损坏")
|
||||
}
|
||||
}
|
||||
|
||||
// 设置默认目录
|
||||
fun setDefaultDirectory(directory: String) {
|
||||
config.defaultDirectory = directory
|
||||
}
|
||||
|
||||
// 设置密码
|
||||
fun setPassword(password: String?) {
|
||||
config.password = password
|
||||
config.encryption = password != null && password.isNotEmpty()
|
||||
|
||||
if (db != null) {
|
||||
if (config.encryption && config.password != null) {
|
||||
run("PRAGMA rekey = ?", listOf(config.password!!))
|
||||
} else {
|
||||
run("PRAGMA rekey = ''", emptyList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查表是否存在
|
||||
fun tableExists(tableName: String): Boolean {
|
||||
val result = query("SELECT name FROM sqlite_master WHERE type='table' AND name=?", listOf(tableName))
|
||||
return result.rows?.isNotEmpty() == true
|
||||
}
|
||||
|
||||
// 创建数据表
|
||||
fun createTable(tableName: String, columns: Map<String, String>): SQLiteResultByKt {
|
||||
val columnDefinitions = columns.entries.joinToString(",") { (name, type) -> "$name $type" }
|
||||
val sql = "CREATE TABLE IF NOT EXISTS $tableName ($columnDefinitions)"
|
||||
return run(sql)
|
||||
}
|
||||
|
||||
// 删除数据表
|
||||
fun dropTable(tableName: String): SQLiteResultByKt {
|
||||
val sql = "DROP TABLE IF EXISTS $tableName"
|
||||
return run(sql)
|
||||
}
|
||||
|
||||
// 关闭数据库
|
||||
fun close() {
|
||||
db?.close()
|
||||
db = null
|
||||
}
|
||||
|
||||
// 开始事务
|
||||
fun beginTransaction(): SQLiteResultByKt {
|
||||
return try {
|
||||
if (isInTransaction) {
|
||||
return SQLiteResultByKt(error = "Transaction already in progress")
|
||||
}
|
||||
db?.beginTransaction()
|
||||
isInTransaction = true
|
||||
SQLiteResultByKt()
|
||||
} catch (e: Exception) {
|
||||
SQLiteResultByKt(error = e.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
fun commit(): SQLiteResultByKt {
|
||||
return try {
|
||||
if (!isInTransaction) {
|
||||
return SQLiteResultByKt(error = "No transaction in progress")
|
||||
}
|
||||
db?.setTransactionSuccessful()
|
||||
db?.endTransaction()
|
||||
isInTransaction = false
|
||||
SQLiteResultByKt()
|
||||
} catch (e: Exception) {
|
||||
SQLiteResultByKt(error = e.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 回滚事务
|
||||
fun rollback(): SQLiteResultByKt {
|
||||
return try {
|
||||
if (!isInTransaction) {
|
||||
return SQLiteResultByKt(error = "No transaction in progress")
|
||||
}
|
||||
db?.endTransaction()
|
||||
isInTransaction = false
|
||||
SQLiteResultByKt()
|
||||
} catch (e: Exception) {
|
||||
SQLiteResultByKt(error = e.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 批量执行SQL
|
||||
fun executeBatch(statements: List<SQLiteExecuteBatchParamsByKt>): List<SQLiteResultByKt> {
|
||||
val results = mutableListOf<SQLiteResultByKt>()
|
||||
var hasError = false
|
||||
|
||||
val wasInTransaction = isInTransaction
|
||||
if (!wasInTransaction) {
|
||||
beginTransaction()
|
||||
}
|
||||
|
||||
try {
|
||||
for (stmt in statements) {
|
||||
val result = run(stmt.sql, stmt.params ?: emptyList())
|
||||
results.add(result)
|
||||
if (result.error != null) {
|
||||
hasError = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (hasError && !wasInTransaction) {
|
||||
rollback()
|
||||
} else if (!wasInTransaction) {
|
||||
commit()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (!wasInTransaction) {
|
||||
rollback()
|
||||
}
|
||||
results.add(SQLiteResultByKt(error = e.message))
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据库文件路径获取前会备份到缓存目录
|
||||
*/
|
||||
fun getDatabasePath(): String? {
|
||||
if (db == null) return null
|
||||
|
||||
try {
|
||||
// 获取当前数据库文件
|
||||
// val dbFile = db!!.path
|
||||
val dbFile = File(db!!.path)
|
||||
if (!dbFile.exists() || !dbFile.canRead()) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 获取缓存目录
|
||||
val cacheDir = context.cacheDir
|
||||
val tempFile = File(cacheDir, "tmui4x_xSqlite_DbackTemp.db")
|
||||
|
||||
// 如果临时文件存在则删除
|
||||
if (tempFile.exists()) {
|
||||
tempFile.delete()
|
||||
}
|
||||
|
||||
// 复制数据库文件到缓存目录
|
||||
dbFile.copyTo(tempFile, overwrite = true)
|
||||
|
||||
return tempFile.absolutePath
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
import { ValueType, ValuesBucket, relationalStore } from '@kit.ArkData';
|
||||
import { featureAbility } from '@kit.AbilityKit';
|
||||
import { BusinessError } from '@ohos.base';
|
||||
interface SQLiteConfig {
|
||||
password ?: string;
|
||||
encryption ?: boolean;
|
||||
locateFile ?: (filename : string) => string;
|
||||
defaultDirectory ?: string;
|
||||
}
|
||||
|
||||
interface SQLiteResult {
|
||||
rows ?: any[][]|null;
|
||||
columns ?: string[]|null;
|
||||
changes ?: number|null;
|
||||
lastInsertRowid ?: number|null;
|
||||
error ?: string|null;
|
||||
maps ?: Map<string, any>[]|null
|
||||
}
|
||||
interface SQLiteExecuteBatchParams {
|
||||
sql : string,
|
||||
params ?: any[]|null
|
||||
}
|
||||
interface SQLiteDbFileInfo {
|
||||
path : string | null,
|
||||
size : number | null
|
||||
}
|
||||
|
||||
export class xSqlite {
|
||||
/** 数据库配置信息 */
|
||||
config : relationalStore.StoreConfig = {
|
||||
name: "sqlite.db",
|
||||
securityLevel: relationalStore.SecurityLevel.S3,
|
||||
customDir: 'tmui4x'
|
||||
};
|
||||
|
||||
private isInTransaction:boolean = false;
|
||||
private db : relationalStore.RdbStore | null = null;
|
||||
|
||||
constructor(config ?: SQLiteConfig) {
|
||||
|
||||
}
|
||||
private converFormat(result?:SQLiteResult):SQLiteResult{
|
||||
if(result==null){
|
||||
return {
|
||||
rows: [] ,
|
||||
columns: [] ,
|
||||
changes: null,
|
||||
lastInsertRowid: null,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
rows: (result?.rows==null?[]:result?.rows),
|
||||
columns: (result?.columns==null?[]:result?.columns),
|
||||
changes: (result?.changes==null?null:result?.changes),
|
||||
maps: (result?.maps==null?null:result?.maps),
|
||||
lastInsertRowid: (result?.lastInsertRowid==null?null:result?.lastInsertRowid),
|
||||
error: (result?.error==null?null:result?.error),
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 创建数据库
|
||||
* @param fileName 数据库文件名,如果为null则使用默认名称'sqlite'
|
||||
* @returns 创建成功返回true,失败返回false
|
||||
*/
|
||||
async createDb(fileName : string | null = null) : Promise<boolean> {
|
||||
if (this.db != null) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
this.config.name = fileName ? (fileName + '.db') : this.config.name
|
||||
const context = UTSHarmony.getUIAbilityContext()
|
||||
return new Promise<boolean>((res, rej) => {
|
||||
relationalStore.getRdbStore(context, this.config).then(async (rdbStore : relationalStore.RdbStore) => {
|
||||
this.db = rdbStore;
|
||||
console.info('Get RdbStore successfully.');
|
||||
res(true)
|
||||
}).catch((err : BusinessError) => {
|
||||
console.error(`Get RdbStore failed, code is ${err.code},message is ${err.message}`);
|
||||
rej(false)
|
||||
});
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 关闭数据库连接
|
||||
* 释放数据库资源,关闭后需要重新创建才能使用
|
||||
*/
|
||||
async close(): Promise<boolean> {
|
||||
if (this.db!=null) {
|
||||
this.db = null;
|
||||
try{
|
||||
await this.db!.close();
|
||||
return Promise.resolve(true)
|
||||
}catch(e){
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
}
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
/**
|
||||
* 鸿蒙不支持,此函数仅为兼容用,执行没有任反应。
|
||||
* @param filename 保存的文件名,可选,默认为'sqlite'
|
||||
* @returns SQLiteResult 保存结果
|
||||
*/
|
||||
saveLocal(filename?: string): SQLiteResult {
|
||||
const context = UTSHarmony.getCurrentWindow()!.getUIContext();
|
||||
console.warn('harmoney不支持本函数')
|
||||
return this.converFormat()
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地文件加载数据库,它同createDb是一样的效果,有就加载,没有就创建。
|
||||
* @param filename 要加载的数据库文件名
|
||||
* @returns SQLiteResult 加载结果
|
||||
*/
|
||||
async loadLocal(filename:string): Promise<SQLiteResult>{
|
||||
try{
|
||||
if(this.db){
|
||||
await this.db.close();
|
||||
}
|
||||
this.config.name = filename+'.db'
|
||||
await this.createDb(this.config.name)
|
||||
return Promise.resolve(this.converFormat())
|
||||
|
||||
}catch(e){
|
||||
|
||||
return Promise.resolve(this.converFormat({error:"错误"} as SQLiteResult))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行SQL语句
|
||||
* @param sql SQL语句
|
||||
* @param params SQL参数数组
|
||||
* @returns SQLiteResult 执行结果,包含changes(影响行数)和lastInsertRowid(最后插入行ID)
|
||||
*/
|
||||
run(sql: string, params: any[] = []): SQLiteResult {
|
||||
try {
|
||||
if (!this.db) return {error:'Database not initialized'}
|
||||
const result = this.db.executeSync(sql,params as relationalStore.ValueType[])
|
||||
console.log('TMUI',`Sqlite:${result}`)
|
||||
return {};
|
||||
} catch (error) {
|
||||
let er = error as BusinessError
|
||||
console.error("TMUI",`Sqlite:${er.message}`)
|
||||
return { error: er.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始事务
|
||||
* 开始一个新的事务,在提交或回滚之前,所有操作都在事务内
|
||||
* @returns SQLiteResult 事务开始结果
|
||||
*/
|
||||
beginTransaction(): SQLiteResult{
|
||||
if (!this.db) return {error:'Database not initialized'}
|
||||
if (this.isInTransaction) {
|
||||
return { error: 'Transaction already in progress' };
|
||||
}
|
||||
this.db.beginTransaction()
|
||||
this.isInTransaction = true;
|
||||
return {};
|
||||
}
|
||||
/**
|
||||
* 提交事务
|
||||
* 提交当前事务的所有操作
|
||||
* @returns SQLiteResult 事务提交结果
|
||||
*/
|
||||
commit(): SQLiteResult {
|
||||
if (!this.db) return {error:'Database not initialized'}
|
||||
if (!this.isInTransaction) {
|
||||
return { error: 'No transaction in progress' };
|
||||
}
|
||||
this.db.commit()
|
||||
this.isInTransaction = false;
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚事务
|
||||
* 撤销当前事务中的所有操作
|
||||
* @returns SQLiteResult 事务回滚结果
|
||||
*/
|
||||
rollback(): SQLiteResult {
|
||||
if (!this.db) return {error:'Database not initialized'}
|
||||
if (!this.isInTransaction) {
|
||||
return { error: 'No transaction in progress' };
|
||||
}
|
||||
this.db.rollBack()
|
||||
this.isInTransaction = false;
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量执行SQL语句
|
||||
* @param statements SQL语句数组,每个元素包含sql和params
|
||||
* @returns SQLiteResult[] 每个语句的执行结果数组
|
||||
*/
|
||||
executeBatch(statements: SQLiteExecuteBatchParams[]): SQLiteResult[] {
|
||||
if (this.isInTransaction) {
|
||||
return [];
|
||||
}
|
||||
const results: SQLiteResult[] = [];
|
||||
let hasError = false;
|
||||
|
||||
// 自动使用事务包装批量操作
|
||||
const wasInTransaction = this.isInTransaction;
|
||||
if (!wasInTransaction) {
|
||||
this.beginTransaction();
|
||||
}
|
||||
|
||||
try {
|
||||
for (const stmt of statements) {
|
||||
const result = this.run(stmt.sql, stmt?.params??[]);
|
||||
results.push(result);
|
||||
if (result.error) {
|
||||
hasError = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有错误并且是我们开始的事务,则回滚
|
||||
if (hasError && !wasInTransaction) {
|
||||
this.rollback();
|
||||
} else if (!wasInTransaction) {
|
||||
// 如果是我们开始的事务且没有错误,则提交
|
||||
this.commit();
|
||||
}
|
||||
} catch (error) {
|
||||
// 发生异常且是我们开始的事务,则回滚
|
||||
if (!wasInTransaction) {
|
||||
this.rollback();
|
||||
}
|
||||
results.push({ error: error.message } as SQLiteResult);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据库文件路径
|
||||
* 获取当前数据库文件的完整路径,获取前会备份到缓存目录
|
||||
* @returns string|null 数据库文件路径,如果数据库未初始化则返回null
|
||||
*/
|
||||
getDatabasePath():string|null{
|
||||
if (this.db==null){
|
||||
console.error('Database not initialized')
|
||||
return null
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前数据库的目录
|
||||
*/
|
||||
setDefaultDirectory(directory: string){
|
||||
console.warn('harmoney不支持本函数')
|
||||
}
|
||||
/**
|
||||
* 设置数据库密码
|
||||
* 空值或者null即删除密码
|
||||
*/
|
||||
setPassword(password: string|null = null){
|
||||
console.warn('harmoney不支持本函数')
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据表
|
||||
* @param tableName 要删除的表名
|
||||
* @returns SQLiteResult 删除结果
|
||||
*/
|
||||
dropTable(tableName: string): SQLiteResult {
|
||||
const sql = `DROP TABLE IF EXISTS ${tableName}`;
|
||||
return this.run(sql)
|
||||
}
|
||||
/**
|
||||
* 查询数据
|
||||
* @param sql 查询SQL语句
|
||||
* @param params 查询参数数组
|
||||
* @returns SQLiteResult 查询结果,包含rows(数据行)和columns(列名)
|
||||
*/
|
||||
query(sql: string, params: any[] = []): SQLiteResult {
|
||||
if (this.db==null){
|
||||
console.error('Database not initialized')
|
||||
return {error:'Database not initialized'}
|
||||
}
|
||||
try {
|
||||
if (this.isInTransaction) {
|
||||
return { error: 'Transaction already in progress' };
|
||||
}
|
||||
const resultSet = this.db.querySqlSync(sql, params as relationalStore.ValueType[]);
|
||||
let maps = [] as Map<string,any>[];
|
||||
let rows:any[][] = [] ;
|
||||
let columns:string[] =[] ;
|
||||
while (resultSet.goToNextRow()) {
|
||||
let newmap = new Map<string,any>();
|
||||
let newrows:any[] = []
|
||||
columns = resultSet.columnNames;
|
||||
resultSet.columnNames.forEach((key:string,index:number)=>{
|
||||
newmap.set(key,resultSet.getValue(index) as any)
|
||||
newrows.push(resultSet.getValue(index) as any)
|
||||
})
|
||||
maps.push(newmap)
|
||||
rows.push(newrows)
|
||||
}
|
||||
return {
|
||||
rows: rows,
|
||||
columns: columns,
|
||||
maps
|
||||
};
|
||||
} catch (error) {
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入数据
|
||||
* @param table 表名
|
||||
* @param data 要插入的数据对象
|
||||
* @returns SQLiteResult 插入结果
|
||||
*/
|
||||
insert(table: string, data: UTSJSONObject): SQLiteResult {
|
||||
if (this.db==null){
|
||||
return {error:'Database not initialized'};
|
||||
}
|
||||
let values:ValuesBucket = {};
|
||||
data.toMap().forEach((value:any,key:string)=>{
|
||||
let keyvalue = value as ValueType;
|
||||
values[key as string] = keyvalue
|
||||
})
|
||||
const result = this.db.insertSync(table,values)
|
||||
const re = result==-1?this.converFormat({error:"操作失败"} as SQLiteResult):(this.converFormat({lastInsertRowid:result} as SQLiteResult))
|
||||
console.log(re)
|
||||
return re
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建数据表
|
||||
* @param tableName 表名
|
||||
* @param columns 列定义对象,key为列名,value为列类型定义
|
||||
* @returns SQLiteResult 创建结果
|
||||
*/
|
||||
createTable(tableName: string, columns: UTSJSONObject): SQLiteResult {
|
||||
const dataMap:string[] = [];
|
||||
columns.toMap().forEach((value:any,key:string)=>{
|
||||
dataMap.push(`${key} ${value}`)
|
||||
})
|
||||
const columnDefinitions = dataMap.join(',');
|
||||
const sql = `CREATE TABLE IF NOT EXISTS ${tableName} (${columnDefinitions})`;
|
||||
return this.run(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新数据
|
||||
* @param table 表名
|
||||
* @param data 要更新的数据对象
|
||||
* @param where WHERE条件语句
|
||||
* @param params WHERE条件参数数组
|
||||
* @returns SQLiteResult 更新结果
|
||||
*/
|
||||
update(table: string, data: UTSJSONObject, where: string, params: any[] = []): SQLiteResult {
|
||||
|
||||
if (this.db==null){
|
||||
return {error:'Database not initialized'};
|
||||
}
|
||||
let keysold:string[] = []
|
||||
let oldValues:any[] = []
|
||||
data.toMap().forEach((value:any,key:string)=>{
|
||||
keysold.push(key)
|
||||
oldValues.push(value)
|
||||
})
|
||||
|
||||
const sets = keysold.map(key => `${key} = ?`).join(',');
|
||||
const values = [...oldValues, ...params];
|
||||
const sql = `UPDATE ${table} SET ${sets} WHERE ${where}`;
|
||||
return this.run(sql, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据
|
||||
* @param table 表名
|
||||
* @param where WHERE条件语句
|
||||
* @param params WHERE条件参数数组
|
||||
* @returns SQLiteResult 删除结果
|
||||
*/
|
||||
delete(table: string, where: string, params: any[] = []): SQLiteResult {
|
||||
const sql = `DELETE FROM ${table} WHERE ${where}`;
|
||||
return this.run(sql, params);
|
||||
}
|
||||
/**
|
||||
* 检查表是否存在
|
||||
* @param tableName 表名
|
||||
* @returns boolean 表存在返回true,不存在返回false
|
||||
*/
|
||||
tableExists(tableName: string): boolean {
|
||||
const result = this.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?", [tableName]);
|
||||
let rows = result?.rows?.length??0
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyTrackingDomains</key>
|
||||
<array/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"deploymentTarget": "12"
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
import {SQLiteConfig,SQLiteResult,SQLiteExecuteBatchParams,SQLiteDbFileInfo} from '../interface.uts';
|
||||
import { URL, URLResourceKey } from 'Foundation';
|
||||
import { Attribute } from 'UIKit';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* SQLite数据库操作类
|
||||
* 提供SQLite数据库的创建、查询、更新等基本操作,支持加密和事务处理
|
||||
*/
|
||||
export class xSqlite {
|
||||
/** 数据库配置信息 */
|
||||
config : SQLiteConfig = {
|
||||
password : null,
|
||||
encryption : false,
|
||||
locateFile:null
|
||||
};
|
||||
private db : xSqliteHelp|null = null;
|
||||
|
||||
constructor(config ?: SQLiteConfig ) {
|
||||
if(config!=null){
|
||||
let realCofing:SQLiteConfig = config!;
|
||||
let directory:null|string = realCofing.defaultDirectory
|
||||
if(typeof directory == 'string' && directory !=null){
|
||||
directory = UTSiOS.convert2AbsFullPath(directory!)
|
||||
}
|
||||
realCofing.defaultDirectory = directory
|
||||
|
||||
this.config = realCofing;
|
||||
}
|
||||
|
||||
this.db = new xSqliteHelp(
|
||||
config = SQLiteConfigBySt(
|
||||
password = this.config.password,
|
||||
encryption = this.config.encryption,
|
||||
defaultDirectory = this.config.defaultDirectory,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private converFormat(result?:SQLiteResultBySt):SQLiteResult{
|
||||
if(result==null){
|
||||
return {
|
||||
rows: [] ,
|
||||
columns: [] ,
|
||||
changes: null,
|
||||
lastInsertRowid: null,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
rows: (result?.rows==null?[]:result?.rows),
|
||||
columns: (result?.columns==null?[]:result?.columns),
|
||||
changes: (result?.changes==null?null:result?.changes),
|
||||
maps: (result?.maps==null?null:result?.maps),
|
||||
lastInsertRowid: (result?.lastInsertRowid==null?null:result?.lastInsertRowid),
|
||||
error: (result?.error==null?null:result?.error),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建数据库
|
||||
* @param fileName 数据库文件名,如果为null则使用默认名称'sqlite'
|
||||
* @returns 创建成功返回true,失败返回false
|
||||
*/
|
||||
createDb(fileName:string|null = null) : boolean{
|
||||
if (this.db==null){
|
||||
return false;
|
||||
}
|
||||
this.db!.createDb(filename = fileName)
|
||||
return true
|
||||
}
|
||||
/**
|
||||
* 执行SQL语句
|
||||
* @param sql SQL语句
|
||||
* @param params SQL参数数组
|
||||
* @returns SQLiteResult 执行结果,包含changes(影响行数)和lastInsertRowid(最后插入行ID)
|
||||
*/
|
||||
run(sql: string, params: any[] = []): SQLiteResult {
|
||||
try {
|
||||
if (this.db==null) throw new Error('Database not initialized');
|
||||
const result = this.db!.run(sql, params = params);
|
||||
return this.converFormat(result)
|
||||
} catch (error) {
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询数据
|
||||
* @param sql 查询SQL语句
|
||||
* @param params 查询参数数组
|
||||
* @returns SQLiteResult 查询结果,包含rows(数据行)和columns(列名)
|
||||
*/
|
||||
query(sql: string, params: Array<any> = [] as any[]): SQLiteResult {
|
||||
if (this.db==null){
|
||||
console.error('Database not initialized')
|
||||
return { error:"Database not initialized" };
|
||||
}
|
||||
const result = this.db!.query(sql, params = params);
|
||||
|
||||
return this.converFormat(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入数据
|
||||
* @param table 表名
|
||||
* @param data 要插入的数据对象
|
||||
* @returns SQLiteResult 插入结果
|
||||
*/
|
||||
insert(table: string, data: UTSJSONObject): SQLiteResult {
|
||||
if (this.db==null){
|
||||
console.error('Database not initialized')
|
||||
return { error:"Database not initialized" };
|
||||
}
|
||||
const dataMap = new Map<string,any>();
|
||||
for(const key in data){
|
||||
const item = data.getAny(key);
|
||||
dataMap.set(key,item)
|
||||
}
|
||||
const result = this.db!.insert(table = table, data = dataMap);
|
||||
return this.converFormat(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新数据
|
||||
* @param table 表名
|
||||
* @param data 要更新的数据对象
|
||||
* @param whereName WHERE条件语句
|
||||
* @param params WHERE条件参数数组
|
||||
* @returns SQLiteResult 更新结果
|
||||
*/
|
||||
update(table: string, data: UTSJSONObject, whereName: string, params: any[] = []): SQLiteResult {
|
||||
if (this.db==null){
|
||||
console.error('Database not initialized')
|
||||
return { error:"Database not initialized" };
|
||||
}
|
||||
const dataMap = new Map<string,any>();
|
||||
for(const key in data){
|
||||
const item = data.getAny(key);
|
||||
dataMap.set(key,item)
|
||||
}
|
||||
const result = this.db!.update(table = table, data = dataMap, where = whereName, params = params);
|
||||
|
||||
return this.converFormat(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据
|
||||
* @param table 表名
|
||||
* @param whereName WHERE条件语句
|
||||
* @param params WHERE条件参数数组
|
||||
* @returns SQLiteResult 删除结果
|
||||
*/
|
||||
delete(table: string, whereName: string, params: any[] = []): SQLiteResult {
|
||||
if (this.db==null){
|
||||
console.error('Database not initialized')
|
||||
return { error:"Database not initialized" };
|
||||
}
|
||||
const result = this.db!.delete(table = table, where = whereName ,params = params);
|
||||
return this.converFormat(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查表是否存在
|
||||
* @param tableName 表名
|
||||
* @returns 表存在返回true,不存在返回false
|
||||
*/
|
||||
tableExists(tableName: string): boolean {
|
||||
if (this.db==null){
|
||||
console.error('Database not initialized')
|
||||
return false;
|
||||
}
|
||||
return this.db!.tableExists(tableName = tableName);
|
||||
}
|
||||
/**
|
||||
* 创建数据表
|
||||
* @param tableName 表名
|
||||
* @param columns 列定义对象,key为列名,value为列类型定义
|
||||
* @returns SQLiteResult 创建结果
|
||||
*/
|
||||
createTable(tableName: string, columns: UTSJSONObject): SQLiteResult {
|
||||
|
||||
const dataMap = new Map<string,string>();
|
||||
for(const key in columns){
|
||||
const item = columns.getString(key)!;
|
||||
dataMap.set(key,item)
|
||||
}
|
||||
console.log('createTabled:',this.db)
|
||||
if (this.db==null){
|
||||
console.error('Database not initialized')
|
||||
return { error:"Database not initialized" };
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const result = this.db!.createTable(tableName = tableName, columns = dataMap);
|
||||
|
||||
return this.converFormat(result)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据表
|
||||
* @param tableName 要删除的表名
|
||||
* @returns SQLiteResult 删除结果
|
||||
*/
|
||||
dropTable(tableName: string): SQLiteResult {
|
||||
if (this.db==null){
|
||||
console.error('Database not initialized')
|
||||
return { error:"Database not initialized" };
|
||||
}
|
||||
const result = this.db!.dropTable(tableName = tableName);
|
||||
return this.converFormat(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭数据库连接
|
||||
* 释放数据库资源,关闭后需要重新创建才能使用
|
||||
*/
|
||||
close(): void {
|
||||
if (this.db!=null) {
|
||||
this.db!.close();
|
||||
this.db = null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 开始事务
|
||||
* 开始一个新的事务,在提交或回滚之前,所有操作都在事务内
|
||||
* @returns SQLiteResult 事务开始结果
|
||||
*/
|
||||
beginTransaction(): SQLiteResult{
|
||||
if (this.db==null){
|
||||
console.error('Database not initialized')
|
||||
return { error:"Database not initialized" };
|
||||
}
|
||||
const result = this.db!.beginTransaction();
|
||||
return this.converFormat(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交事务
|
||||
* 提交当前事务的所有操作
|
||||
* @returns SQLiteResult 事务提交结果
|
||||
*/
|
||||
commit(): SQLiteResult {
|
||||
if (this.db==null){
|
||||
console.error('Database not initialized')
|
||||
return { error:"Database not initialized" };
|
||||
}
|
||||
const result = this.db!.commit();
|
||||
return this.converFormat(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚事务
|
||||
* 撤销当前事务中的所有操作
|
||||
* @returns SQLiteResult 事务回滚结果
|
||||
*/
|
||||
rollback(): SQLiteResult {
|
||||
if (this.db==null){
|
||||
console.error('Database not initialized')
|
||||
return { error:"Database not initialized" };
|
||||
}
|
||||
const result = this.db!.rollback();
|
||||
return this.converFormat(result)
|
||||
}
|
||||
/**
|
||||
* 批量执行SQL语句
|
||||
* @param statements SQL语句数组,每个元素包含sql和params
|
||||
* @returns SQLiteResult[] 每个语句的执行结果数组
|
||||
*/
|
||||
executeBatch(statements: SQLiteExecuteBatchParams[]): SQLiteResult[] {
|
||||
if (this.db==null){
|
||||
console.error('Database not initialized')
|
||||
return [];
|
||||
}
|
||||
const list = new Array<SQLiteExecuteBatchParamsBySt>()
|
||||
for(const item in statements){
|
||||
list.push(SQLiteExecuteBatchParamsBySt(
|
||||
sql = item.sql,
|
||||
params = item.params
|
||||
))
|
||||
}
|
||||
const result = this.db!.executeBatch(statements = list);
|
||||
let resultList = [] as SQLiteResult[]
|
||||
for(const item in result){
|
||||
resultList.push(this.converFormat(item))
|
||||
}
|
||||
|
||||
return resultList
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 保存数据库到本地文件
|
||||
* @param filename 保存的文件名,可选,默认为'sqlite'
|
||||
* @returns SQLiteResult 保存结果
|
||||
*/
|
||||
saveLocal(filename?: string): SQLiteResult {
|
||||
if (this.db==null){
|
||||
return {error:'Database not initialized'};
|
||||
}
|
||||
const result = this.db!.saveLocal(filename = filename);
|
||||
return this.converFormat(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地文件加载数据库
|
||||
* @param filename 要加载的数据库文件名
|
||||
* @returns SQLiteResult 加载结果
|
||||
*/
|
||||
loadLocal(filename:string): SQLiteResult{
|
||||
const result = this.db!.loadLocal(filename = filename);
|
||||
return this.converFormat(result)
|
||||
}
|
||||
/**
|
||||
* 获取数据库文件路径获取前会备份到缓存目录
|
||||
*/
|
||||
getDatabasePath():string|null{
|
||||
if (this.db==null){
|
||||
console.error('Database not initialized')
|
||||
return null
|
||||
}
|
||||
const result = this.db!.getDatabasePath();
|
||||
return result
|
||||
}
|
||||
/**
|
||||
* 当前数据库的目录
|
||||
*/
|
||||
setDefaultDirectory(directory: string){
|
||||
if (this.db==null){
|
||||
console.error('Database not initialized')
|
||||
return
|
||||
}
|
||||
let rdirectory = directory;
|
||||
if(rdirectory!=''){
|
||||
rdirectory = UTSiOS.convert2AbsFullPath(directory)
|
||||
}
|
||||
this.db!.setDefaultDirectory(directory = rdirectory);
|
||||
}
|
||||
/**
|
||||
* 设置数据库密码
|
||||
* 空值或者null即删除密码
|
||||
*/
|
||||
setPassword(password: string|null = null){
|
||||
if (this.db==null){
|
||||
console.error('Database not initialized')
|
||||
return
|
||||
}
|
||||
this.db!.setPassword(password);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
import Foundation
|
||||
import SQLite3
|
||||
// UTS内置对象的引用
|
||||
import DCloudUTSFoundation
|
||||
|
||||
struct SQLiteConfigBySt {
|
||||
var password: String?
|
||||
var encryption: Bool
|
||||
var locateFile: ((String) -> String)?
|
||||
var defaultDirectory: String?
|
||||
|
||||
init(password: String? = nil, encryption: Bool = false, locateFile: ((String) -> String)? = nil, defaultDirectory: String? = nil) {
|
||||
self.password = password
|
||||
self.encryption = encryption
|
||||
self.locateFile = locateFile
|
||||
self.defaultDirectory = defaultDirectory
|
||||
}
|
||||
}
|
||||
|
||||
struct SQLiteResultBySt {
|
||||
var rows: [[Any]]?
|
||||
var columns: [String]?
|
||||
var changes: Int?
|
||||
var lastInsertRowid: Int64?
|
||||
var error: String?
|
||||
var maps: [[String: Any]]?
|
||||
}
|
||||
|
||||
struct SQLiteExecuteBatchParamsBySt {
|
||||
var sql: String
|
||||
var params: [Any]?
|
||||
}
|
||||
|
||||
class xSqliteHelp {
|
||||
private var db: OpaquePointer?
|
||||
private var isInTransaction: Bool = false
|
||||
private var config: SQLiteConfigBySt
|
||||
private var fileURLPath: URL?
|
||||
init(config: SQLiteConfigBySt = SQLiteConfigBySt()) {
|
||||
self.config = config
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 创建数据库
|
||||
func createDb(filename: String? = nil) -> OpaquePointer? {
|
||||
let fname = filename ?? "sqlite"
|
||||
let fileURL: URL
|
||||
if let defaultDir = config.defaultDirectory {
|
||||
fileURL = URL(fileURLWithPath: defaultDir).appendingPathComponent("\(fname).db")
|
||||
// 检查目录是否存在,不存在则递归创建
|
||||
let dirURL = fileURL.deletingLastPathComponent()
|
||||
let fileManager = FileManager.default
|
||||
if !fileManager.fileExists(atPath: dirURL.path) {
|
||||
do {
|
||||
try fileManager.createDirectory(at: dirURL, withIntermediateDirectories: true, attributes: nil)
|
||||
} catch {
|
||||
// print("创建目录失败: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fileURL = try! FileManager.default
|
||||
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
|
||||
.appendingPathComponent("\(fname).db")
|
||||
}
|
||||
|
||||
var dbPointer: OpaquePointer?
|
||||
if sqlite3_open(fileURL.path, &dbPointer) == SQLITE_OK {
|
||||
db = dbPointer
|
||||
if config.encryption && config.password != nil {
|
||||
_ = run("PRAGMA key = ?", params: [config.password!])
|
||||
}
|
||||
self.fileURLPath = fileURL
|
||||
return db
|
||||
} else {
|
||||
sqlite3_close(dbPointer)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 执行SQL查询
|
||||
func run(_ sql: String, params: [Any] = []) -> SQLiteResultBySt {
|
||||
guard let db = db else {
|
||||
return SQLiteResultBySt(error: "Database not initialized")
|
||||
}
|
||||
|
||||
var statement: OpaquePointer?
|
||||
|
||||
if sqlite3_prepare_v2(db, sql, -1, &statement, nil) != SQLITE_OK {
|
||||
let errmsg = String(cString: sqlite3_errmsg(db))
|
||||
return SQLiteResultBySt(error: errmsg)
|
||||
}
|
||||
|
||||
for (index, param) in params.enumerated() {
|
||||
let idx = Int32(index + 1)
|
||||
|
||||
switch param {
|
||||
case let text as String:
|
||||
sqlite3_bind_text(statement, idx, (text as NSString).utf8String, -1, nil)
|
||||
case let num as Int:
|
||||
sqlite3_bind_int64(statement, idx, Int64(num))
|
||||
case let num as Int64:
|
||||
sqlite3_bind_int64(statement, idx, num)
|
||||
case let num as Double:
|
||||
sqlite3_bind_double(statement, idx, num)
|
||||
case let data as Data:
|
||||
data.withUnsafeBytes { bytes in
|
||||
sqlite3_bind_blob(statement, idx, bytes.baseAddress, Int32(data.count), nil)
|
||||
}
|
||||
case is NSNull:
|
||||
sqlite3_bind_null(statement, idx)
|
||||
default:
|
||||
"\(param)".utf8CString.withUnsafeBufferPointer { cString in
|
||||
sqlite3_bind_text(statement, idx, cString.baseAddress, -1, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = sqlite3_step(statement)
|
||||
|
||||
if result != SQLITE_DONE && result != SQLITE_ROW {
|
||||
let errmsg = String(cString: sqlite3_errmsg(db))
|
||||
sqlite3_finalize(statement)
|
||||
return SQLiteResultBySt(error: errmsg)
|
||||
}
|
||||
|
||||
let changes = sqlite3_changes(db)
|
||||
let lastId = sqlite3_last_insert_rowid(db)
|
||||
|
||||
sqlite3_finalize(statement)
|
||||
|
||||
return SQLiteResultBySt(changes: Int(changes), lastInsertRowid: lastId)
|
||||
}
|
||||
|
||||
// 查询数据
|
||||
func query(_ sql: String, params: [Any] = []) -> SQLiteResultBySt {
|
||||
guard let db = db else {
|
||||
return SQLiteResultBySt(error: "Database not initialized")
|
||||
}
|
||||
|
||||
var statement: OpaquePointer?
|
||||
|
||||
if sqlite3_prepare_v2(db, sql, -1, &statement, nil) != SQLITE_OK {
|
||||
let errmsg = String(cString: sqlite3_errmsg(db))
|
||||
return SQLiteResultBySt(error: errmsg)
|
||||
}
|
||||
|
||||
for (index, param) in params.enumerated() {
|
||||
let idx = Int32(index + 1)
|
||||
|
||||
switch param {
|
||||
case let text as String:
|
||||
sqlite3_bind_text(statement, idx, (text as NSString).utf8String, -1, nil)
|
||||
case let num as Int:
|
||||
sqlite3_bind_int64(statement, idx, Int64(num))
|
||||
case let num as Int64:
|
||||
sqlite3_bind_int64(statement, idx, num)
|
||||
case let num as Double:
|
||||
sqlite3_bind_double(statement, idx, num)
|
||||
case let data as Data:
|
||||
data.withUnsafeBytes { bytes in
|
||||
sqlite3_bind_blob(statement, idx, bytes.baseAddress, Int32(data.count), nil)
|
||||
}
|
||||
case is NSNull:
|
||||
sqlite3_bind_null(statement, idx)
|
||||
default:
|
||||
"\(param)".utf8CString.withUnsafeBufferPointer { cString in
|
||||
sqlite3_bind_text(statement, idx, cString.baseAddress, -1, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var rows: [[Any]] = []
|
||||
var columns: [String] = []
|
||||
var maps: [[String: Any]] = []
|
||||
|
||||
// 获取列名
|
||||
let columnCount = sqlite3_column_count(statement)
|
||||
for i in 0..<columnCount {
|
||||
if let name = sqlite3_column_name(statement, i) {
|
||||
columns.append(String(cString: name))
|
||||
}
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
while sqlite3_step(statement) == SQLITE_ROW {
|
||||
var row: [Any] = []
|
||||
var map: [String: Any] = [:]
|
||||
|
||||
for i in 0..<columnCount {
|
||||
let type = sqlite3_column_type(statement, i)
|
||||
let columnName = columns[Int(i)]
|
||||
var value: Any = ""
|
||||
|
||||
switch type {
|
||||
case SQLITE_TEXT:
|
||||
if let text = sqlite3_column_text(statement, i) {
|
||||
value = String(cString: text)
|
||||
}
|
||||
case SQLITE_INTEGER:
|
||||
value = sqlite3_column_int64(statement, i)
|
||||
case SQLITE_FLOAT:
|
||||
value = sqlite3_column_double(statement, i)
|
||||
case SQLITE_BLOB:
|
||||
let blobLength = sqlite3_column_bytes(statement, i)
|
||||
if let blobData = sqlite3_column_blob(statement, i) {
|
||||
value = Data(bytes: blobData, count: Int(blobLength))
|
||||
} else {
|
||||
value = Data()
|
||||
}
|
||||
default:
|
||||
value = ""
|
||||
}
|
||||
|
||||
row.append(value)
|
||||
map[columnName] = value
|
||||
}
|
||||
|
||||
rows.append(row)
|
||||
maps.append(map)
|
||||
}
|
||||
|
||||
sqlite3_finalize(statement)
|
||||
|
||||
return SQLiteResultBySt(rows: rows, columns: columns, maps: maps)
|
||||
}
|
||||
|
||||
// 插入数据
|
||||
func insert(table: String, data: [String: Any]) -> SQLiteResultBySt {
|
||||
let keys = Array(data.keys)
|
||||
let values = keys.map { data[$0]! }
|
||||
|
||||
let sql = "INSERT INTO \(table) (\(keys.joined(separator: ","))) VALUES (\(keys.map { _ in "?" }.joined(separator: ",")))"
|
||||
return run(sql, params: values)
|
||||
}
|
||||
|
||||
// 更新数据
|
||||
func update(table: String, data: [String: Any], where: String, params: [Any] = []) -> SQLiteResultBySt {
|
||||
let keys = Array(data.keys)
|
||||
var values = keys.map { data[$0]! }
|
||||
|
||||
let sets = keys.map { "\($0) = ?" }.joined(separator: ",")
|
||||
values.append(contentsOf: params)
|
||||
|
||||
let sql = "UPDATE \(table) SET \(sets) WHERE \(`where`)"
|
||||
return run(sql, params: values)
|
||||
}
|
||||
|
||||
// 删除数据
|
||||
func delete(table: String, where: String, params: [Any] = []) -> SQLiteResultBySt {
|
||||
let sql = "DELETE FROM \(table) WHERE \(`where`)"
|
||||
return run(sql, params: params)
|
||||
}
|
||||
|
||||
// 保存数据库到本地
|
||||
func saveLocal(filename: String? = nil) -> SQLiteResultBySt {
|
||||
guard let db = self.db else {
|
||||
return SQLiteResultBySt(error: "Database not initialized")
|
||||
}
|
||||
|
||||
do {
|
||||
let fname = filename ?? "sqlite"
|
||||
let fileManager = FileManager.default
|
||||
|
||||
// 使用配置的默认目录或系统文档目录
|
||||
let targetDir = self.config.defaultDirectory != nil ? URL(fileURLWithPath: self.config.defaultDirectory!) : fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
|
||||
// 获取当前数据库文件的路径
|
||||
var dbPath = String(cString: sqlite3_db_filename(self.db!, nil))
|
||||
|
||||
// 如果dbPath为空,则使用fileURLPath或默认路径
|
||||
if dbPath.isEmpty {
|
||||
if let fileURL = self.fileURLPath {
|
||||
dbPath = fileURL.path
|
||||
} else {
|
||||
return SQLiteResultBySt(error: "无法获取数据库文件路径")
|
||||
}
|
||||
}
|
||||
|
||||
let sourceUrl = URL(fileURLWithPath: dbPath)
|
||||
let targetUrl = targetDir.appendingPathComponent("\(fname).db")
|
||||
|
||||
// 检查源文件是否存在和可读
|
||||
if !fileManager.fileExists(atPath: sourceUrl.path) {
|
||||
return SQLiteResultBySt(error: "未能找到数据库文件")
|
||||
}
|
||||
|
||||
guard let sourceAttrs = try? fileManager.attributesOfItem(atPath: sourceUrl.path),
|
||||
sourceAttrs[.size] as? UInt64 ?? 0 > 0 else {
|
||||
return SQLiteResultBySt(error: "数据库文件无效或损坏")
|
||||
}
|
||||
|
||||
// 确保数据库处于一致状态
|
||||
sqlite3_exec(db, "PRAGMA wal_checkpoint(FULL)", nil, nil, nil)
|
||||
|
||||
// 创建临时文件
|
||||
let tempUrl = targetDir.appendingPathComponent("\(fname)_temp.db")
|
||||
|
||||
// 如果临时文件存在则删除
|
||||
if fileManager.fileExists(atPath: tempUrl.path) {
|
||||
try fileManager.removeItem(at: tempUrl)
|
||||
}
|
||||
|
||||
// 先复制到临时文件
|
||||
try fileManager.copyItem(at: sourceUrl, to: tempUrl)
|
||||
|
||||
// 验证临时文件
|
||||
guard let tempAttrs = try? fileManager.attributesOfItem(atPath: tempUrl.path),
|
||||
tempAttrs[.size] as? UInt64 ?? 0 > 0 else {
|
||||
try? fileManager.removeItem(at: tempUrl)
|
||||
return SQLiteResultBySt(error: "备份文件创建失败")
|
||||
}
|
||||
|
||||
// 如果目标文件存在则删除
|
||||
if fileManager.fileExists(atPath: targetUrl.path) {
|
||||
try fileManager.removeItem(at: targetUrl)
|
||||
}
|
||||
|
||||
// 将临时文件移动到目标位置
|
||||
try fileManager.moveItem(at: tempUrl, to: targetUrl)
|
||||
|
||||
return SQLiteResultBySt()
|
||||
} catch {
|
||||
return SQLiteResultBySt(error: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
// 从本地加载数据库
|
||||
func loadLocal(filename: String) -> SQLiteResultBySt {
|
||||
let fileManager = FileManager.default
|
||||
let sourceDir = self.config.defaultDirectory != nil ? URL(fileURLWithPath: self.config.defaultDirectory!) : fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
let sourceUrl = sourceDir.appendingPathComponent("\(filename).db")
|
||||
let targetUrl = sourceDir.appendingPathComponent("sqlite.db")
|
||||
|
||||
do {
|
||||
// 检查源文件是否存在和可读
|
||||
if !fileManager.fileExists(atPath: sourceUrl.path) {
|
||||
return SQLiteResultBySt(error: "没有数据库")
|
||||
}
|
||||
|
||||
guard let sourceAttrs = try? fileManager.attributesOfItem(atPath: sourceUrl.path),
|
||||
sourceAttrs[.size] as? UInt64 ?? 0 > 0 else {
|
||||
return SQLiteResultBySt(error: "数据库文件无效或损坏")
|
||||
}
|
||||
|
||||
// 创建临时文件
|
||||
let tempUrl = sourceDir.appendingPathComponent("\(filename)_temp.db")
|
||||
|
||||
// 如果临时文件存在则删除
|
||||
if fileManager.fileExists(atPath: tempUrl.path) {
|
||||
try fileManager.removeItem(at: tempUrl)
|
||||
}
|
||||
|
||||
// 先复制到临时文件
|
||||
try fileManager.copyItem(at: sourceUrl, to: tempUrl)
|
||||
|
||||
// 验证临时文件
|
||||
guard let tempAttrs = try? fileManager.attributesOfItem(atPath: tempUrl.path),
|
||||
tempAttrs[.size] as? UInt64 ?? 0 > 0 else {
|
||||
try? fileManager.removeItem(at: tempUrl)
|
||||
return SQLiteResultBySt(error: "数据库文件损坏")
|
||||
}
|
||||
|
||||
// 如果目标文件存在则删除
|
||||
if fileManager.fileExists(atPath: targetUrl.path) {
|
||||
try fileManager.removeItem(at: targetUrl)
|
||||
}
|
||||
|
||||
// 将临时文件移动到目标位置
|
||||
try fileManager.moveItem(at: tempUrl, to: targetUrl)
|
||||
|
||||
// 尝试打开数据库
|
||||
if let _ = self.createDb() {
|
||||
return SQLiteResultBySt()
|
||||
} else {
|
||||
return SQLiteResultBySt(error: "数据库文件损坏")
|
||||
}
|
||||
} catch {
|
||||
// 清理临时文件
|
||||
let tempUrl = sourceDir.appendingPathComponent("\(filename)_temp.db")
|
||||
try? fileManager.removeItem(at: tempUrl)
|
||||
|
||||
return SQLiteResultBySt(error: "数据库文件损坏")
|
||||
}
|
||||
}
|
||||
|
||||
// 设置默认目录
|
||||
func setDefaultDirectory(directory: String) {
|
||||
self.config.defaultDirectory = directory
|
||||
}
|
||||
|
||||
// 设置密码
|
||||
func setPassword(_ password: String?) {
|
||||
config.password = password
|
||||
config.encryption = password != nil && !password!.isEmpty
|
||||
}
|
||||
|
||||
// 检查表是否存在
|
||||
func tableExists(tableName: String) -> Bool {
|
||||
let result = query("SELECT name FROM sqlite_master WHERE type='table' AND name=?", params: [tableName])
|
||||
return (result.rows?.isEmpty == false)
|
||||
}
|
||||
|
||||
// 创建数据表
|
||||
func createTable(tableName: String, columns: [String: String]) -> SQLiteResultBySt {
|
||||
let columnDefinitions = columns.map { key, value in "\(key) \(value)" }.joined(separator: ",")
|
||||
let sql = "CREATE TABLE IF NOT EXISTS \(tableName) (\(columnDefinitions))"
|
||||
return run(sql)
|
||||
}
|
||||
|
||||
// 删除数据表
|
||||
func dropTable(tableName: String) -> SQLiteResultBySt {
|
||||
let sql = "DROP TABLE IF EXISTS \(tableName)"
|
||||
return run(sql)
|
||||
}
|
||||
|
||||
// 关闭数据库
|
||||
func close() {
|
||||
if let db = db {
|
||||
sqlite3_close(db)
|
||||
self.db = nil
|
||||
}
|
||||
}
|
||||
|
||||
// 开始事务
|
||||
func beginTransaction() -> SQLiteResultBySt {
|
||||
if isInTransaction {
|
||||
return SQLiteResultBySt(error: "Transaction already in progress")
|
||||
}
|
||||
|
||||
isInTransaction = true
|
||||
return run("BEGIN TRANSACTION")
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
func commit() -> SQLiteResultBySt {
|
||||
if !isInTransaction {
|
||||
return SQLiteResultBySt(error: "No transaction in progress")
|
||||
}
|
||||
|
||||
isInTransaction = false
|
||||
return run("COMMIT")
|
||||
}
|
||||
|
||||
// 回滚事务
|
||||
func rollback() -> SQLiteResultBySt {
|
||||
if !isInTransaction {
|
||||
return SQLiteResultBySt(error: "No transaction in progress")
|
||||
}
|
||||
|
||||
isInTransaction = false
|
||||
return run("ROLLBACK")
|
||||
}
|
||||
|
||||
// 批量执行SQL
|
||||
func executeBatch(statements: [SQLiteExecuteBatchParamsBySt]) -> [SQLiteResultBySt] {
|
||||
var results: [SQLiteResultBySt] = []
|
||||
var hasError = false
|
||||
|
||||
let wasInTransaction = isInTransaction
|
||||
if !wasInTransaction {
|
||||
_ = beginTransaction()
|
||||
}
|
||||
|
||||
for stmt in statements {
|
||||
let result = run(stmt.sql, params: stmt.params ?? [])
|
||||
results.append(result)
|
||||
|
||||
if result.error != nil {
|
||||
hasError = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hasError && !wasInTransaction {
|
||||
_ = rollback()
|
||||
} else if !wasInTransaction {
|
||||
_ = commit()
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
// 获取数据库文件路径
|
||||
func getDatabasePath() -> String? {
|
||||
let fileManager = FileManager.default
|
||||
guard let db = self.db else {
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
// 获取当前数据库文件路径
|
||||
var dbPath = String(cString: sqlite3_db_filename(self.db!, nil))
|
||||
|
||||
// 如果dbPath为空,则使用fileURLPath或默认路径
|
||||
if dbPath.isEmpty {
|
||||
if let fileURL = self.fileURLPath {
|
||||
dbPath = fileURL.path
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 获取缓存目录
|
||||
let cacheURL = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||||
let tempURL = cacheURL.appendingPathComponent("tmui4x_xSqlite_DbackTemp.db")
|
||||
|
||||
// 如果临时文件存在则删除
|
||||
if fileManager.fileExists(atPath: tempURL.path) {
|
||||
try fileManager.removeItem(at: tempURL)
|
||||
}
|
||||
|
||||
// 复制数据库文件到缓存目录
|
||||
try FileManager.default.copyItem(atPath: dbPath, toPath: tempURL.path)
|
||||
|
||||
return tempURL.path
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
export type SQLiteConfig = {
|
||||
password?: string;
|
||||
encryption?: boolean;
|
||||
locateFile?: (filename: string) => string;
|
||||
defaultDirectory?: string;
|
||||
}
|
||||
|
||||
export type SQLiteResult = {
|
||||
rows?: any[][];
|
||||
columns?: string[];
|
||||
changes?: number;
|
||||
lastInsertRowid?: number;
|
||||
error?: string;
|
||||
maps?:Map<string,any>[]
|
||||
}
|
||||
export type SQLiteExecuteBatchParams = {
|
||||
sql:string,
|
||||
params?:any[]
|
||||
}
|
||||
export type SQLiteDbFileInfo = {
|
||||
path:string|null,
|
||||
size:number|null
|
||||
}
|
||||
@@ -0,0 +1,481 @@
|
||||
export type SQLiteConfig = {
|
||||
password?: string;
|
||||
encryption?: boolean;
|
||||
locateFile?: (filename: string) => string;
|
||||
}
|
||||
|
||||
export type SQLiteResult = {
|
||||
rows?: any[];
|
||||
columns?: string[];
|
||||
changes?: number;
|
||||
lastInsertRowid?: number;
|
||||
error?: string;
|
||||
maps?:Map<string,any>[]
|
||||
}
|
||||
export type SQLiteExecuteBatchParams = {
|
||||
sql:string,
|
||||
params?:any[]
|
||||
}
|
||||
|
||||
async function loadScript(url) {
|
||||
return new Promise(res => {
|
||||
var script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.src = url;
|
||||
|
||||
// 当脚本加载完成后,执行回调
|
||||
script.onload = function () {
|
||||
res()
|
||||
};
|
||||
|
||||
// 处理旧版浏览器的onreadystatechange事件
|
||||
script.onreadystatechange = function () {
|
||||
if (this.readyState === 'loaded' || this.readyState === 'complete') {
|
||||
script.onload();
|
||||
}
|
||||
};
|
||||
|
||||
// 将脚本添加到head中,开始加载
|
||||
document.head.appendChild(script);
|
||||
})
|
||||
}
|
||||
|
||||
async function loadScripts(scripts) {
|
||||
await (async function loadNextScript(i) {
|
||||
if (i < scripts.length) {
|
||||
await loadScript(scripts[i]);
|
||||
await loadNextScript(i + 1)
|
||||
}
|
||||
})(0);
|
||||
}
|
||||
|
||||
let jsFiles = [
|
||||
'/static/tmui4xLibs/lib/sql-wasm.js'
|
||||
];
|
||||
|
||||
/**
|
||||
* SQLite数据库操作类
|
||||
* 提供SQLite数据库的创建、查询、更新等基本操作,支持加密和事务处理
|
||||
*/
|
||||
export class xSqlite {
|
||||
/** 数据库实例 */
|
||||
private db: any|null = null;
|
||||
private config: SQLiteConfig;
|
||||
private isInTransaction: boolean = false;
|
||||
private SQL:any|null = null;
|
||||
private newFileName:string = "sqlite"
|
||||
|
||||
constructor(configs: SQLiteConfig = {}) {
|
||||
let locateFile = filename => `/static/tmui4xLibs/lib/sql-wasm.wasm`
|
||||
this.config = {locateFile,...configs};
|
||||
this.config.locateFile = this.config.locateFile||locateFile
|
||||
}
|
||||
|
||||
private _loadSqliteJs(): Promise<any>{
|
||||
let _this = this;
|
||||
return new Promise((res, rej) => {
|
||||
loadScripts(jsFiles).then(() => {
|
||||
initSqlJs(_this.config).then((SQL) => {
|
||||
_this.SQL = SQL;
|
||||
res();
|
||||
}).catch(e => rej(e))
|
||||
}).catch(e => rej(e))
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 创建数据库
|
||||
* @param filename 数据库文件名,如果为null则使用默认名称'sqlite'
|
||||
* @returns Promise<any> 创建的数据库实例
|
||||
*/
|
||||
async createDb(filename?:string): Promise<any> {
|
||||
let _this = this;
|
||||
this.newFileName = filename||"sqlite"
|
||||
return new Promise((res, rej) => {
|
||||
_this._loadSqliteJs()
|
||||
.then(_=>{
|
||||
_this.db = new _this.SQL.Database();
|
||||
// 如果配置了加密
|
||||
if (this.config.encryption && _this.config.password) {
|
||||
_this.run('PRAGMA key = ?', [_this.config.password]);
|
||||
}
|
||||
})
|
||||
.catch(_=>{
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行SQL语句
|
||||
* @param sql SQL语句
|
||||
* @param params SQL参数数组
|
||||
* @returns SQLiteResult 执行结果,包含changes(影响行数)和lastInsertRowid(最后插入行ID)
|
||||
*/
|
||||
run(sql: string, params: any[] = []): SQLiteResult {
|
||||
try {
|
||||
if (this.isInTransaction) {
|
||||
return { error: 'Transaction already in progress' };
|
||||
}
|
||||
const result = this.db.run(sql, params);
|
||||
return {
|
||||
changes: result?.changes,
|
||||
lastInsertRowid: result?.lastInsertRowid
|
||||
};
|
||||
} catch (error) {
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询数据
|
||||
* @param sql 查询SQL语句
|
||||
* @param params 查询参数数组
|
||||
* @returns SQLiteResult 查询结果,包含rows(数据行)和columns(列名)
|
||||
*/
|
||||
query(sql: string, params: any[] = []): SQLiteResult {
|
||||
try {
|
||||
if (this.isInTransaction) {
|
||||
return { error: 'Transaction already in progress' };
|
||||
}
|
||||
const result = this.db.exec(sql, params);
|
||||
const maps = [] as Map<string,any>[];
|
||||
const rows = result?.[0]?.values || [] ;
|
||||
const columns = result?.[0]?.columns || [] ;
|
||||
if(rows.length>0&&columns.length>0){
|
||||
rows.forEach(el=>{
|
||||
let newmap = new Map<string,any>();
|
||||
columns.forEach((key,index)=>{
|
||||
newmap.set(key,el[index])
|
||||
})
|
||||
maps.push(newmap)
|
||||
})
|
||||
}
|
||||
return {
|
||||
rows: rows,
|
||||
columns: columns,
|
||||
maps
|
||||
};
|
||||
} catch (error) {
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入数据
|
||||
* @param table 表名
|
||||
* @param data 要插入的数据对象
|
||||
* @returns SQLiteResult 插入结果
|
||||
*/
|
||||
insert(table: string, data: object): SQLiteResult {
|
||||
const keys = Object.keys(data);
|
||||
const values = Object.values(data);
|
||||
const sql = `INSERT INTO ${table} (${keys.join(',')}) VALUES (${keys.map(() => '?').join(',')})`;
|
||||
return this.run(sql, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新数据
|
||||
* @param table 表名
|
||||
* @param data 要更新的数据对象
|
||||
* @param where WHERE条件语句
|
||||
* @param params WHERE条件参数数组
|
||||
* @returns SQLiteResult 更新结果
|
||||
*/
|
||||
update(table: string, data: object, where: string, params: any[] = []): SQLiteResult {
|
||||
const sets = Object.keys(data).map(key => `${key} = ?`).join(',');
|
||||
const values = [...Object.values(data), ...params];
|
||||
const sql = `UPDATE ${table} SET ${sets} WHERE ${where}`;
|
||||
return this.run(sql, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据
|
||||
* @param table 表名
|
||||
* @param where WHERE条件语句
|
||||
* @param params WHERE条件参数数组
|
||||
* @returns SQLiteResult 删除结果
|
||||
*/
|
||||
delete(table: string, where: string, params: any[] = []): SQLiteResult {
|
||||
const sql = `DELETE FROM ${table} WHERE ${where}`;
|
||||
return this.run(sql, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存数据库到本地存储
|
||||
* @param filename 保存的文件名,可选,默认为'sqlite'
|
||||
* @returns SQLiteResult 保存结果
|
||||
*/
|
||||
saveLocal(filename?: string): SQLiteResult {
|
||||
if (this.db==null) throw new Error('Database not initialized');
|
||||
let fname = filename??'sqlite'
|
||||
const data = this.db.export();
|
||||
this.newFileName = fname;
|
||||
// const blob = new Blob([data], { type: 'application/octet-stream' });
|
||||
// const url = window.URL.createObjectURL(blob);
|
||||
|
||||
const base64String = btoa(String.fromCharCode(...data));
|
||||
// const a = document.createElement('a');
|
||||
// a.href = url;
|
||||
// a.download = filename;
|
||||
// document.body.appendChild(a);
|
||||
// a.click();
|
||||
// a.remove();
|
||||
// window.URL.revokeObjectURL(url);
|
||||
if(base64String.length>5*1024*1024){
|
||||
console.error("数据过大")
|
||||
return {error:"数据过大"};
|
||||
}
|
||||
try {
|
||||
uni.setStorageSync(fname,base64String)
|
||||
} catch (error) {
|
||||
return {error:"数据过大"};
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
async loadLocal(filename: string): Promise<SQLiteResult> {
|
||||
this.newFileName = filename;
|
||||
const data = uni.getStorageSync(filename)
|
||||
if(data==null){
|
||||
console.error("没有数据库")
|
||||
return Promise.reject({error:"没有数据库"})
|
||||
}
|
||||
try {
|
||||
const buffer = Uint8Array.from(atob(data), c => c.charCodeAt(0));
|
||||
await this._loadSqliteJs()
|
||||
this.loadFromBuffer(buffer);
|
||||
return Promise.resolve({})
|
||||
} catch (error) {
|
||||
return Promise.reject({error:"数据库损坏"})
|
||||
}
|
||||
return Promise.reject({error:"没有数据库"})
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件加载数据库
|
||||
* @param file 要加载的数据库文件
|
||||
* @returns Promise<void> 加载完成的Promise
|
||||
*/
|
||||
async loadFromFile(file: File): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async event => {
|
||||
try {
|
||||
const uInt8Array = new Uint8Array(event.target.result as ArrayBuffer);
|
||||
await this.loadFromBuffer(uInt8Array);
|
||||
resolve();
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
reader.onerror = () => reject(reader.error);
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从远程URL加载数据库
|
||||
* @param url 数据库文件的URL地址
|
||||
* @returns Promise<void> 加载完成的Promise
|
||||
*/
|
||||
async loadFromUrl(url: string): Promise<void> {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
const buffer = await response.arrayBuffer();
|
||||
await this.loadFromBuffer(new Uint8Array(buffer));
|
||||
}
|
||||
|
||||
// 从Buffer加载数据库
|
||||
loadFromBuffer(buffer: Uint8Array): void{
|
||||
if (this.SQL==null) throw new Error('Database not initialized');
|
||||
this.db = new this.SQL.Database(buffer);
|
||||
// 如果配置了加密
|
||||
if (this.config.encryption && this.config.password) {
|
||||
this.run('PRAGMA key = ?', [this.config.password]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查表是否存在
|
||||
* @param tableName 表名
|
||||
* @returns boolean 表存在返回true,不存在返回false
|
||||
*/
|
||||
tableExists(tableName: string): boolean {
|
||||
const result = this.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?", [tableName]);
|
||||
return result.rows && result.rows.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建数据表
|
||||
* @param tableName 表名
|
||||
* @param columns 列定义对象,key为列名,value为列类型定义
|
||||
* @returns SQLiteResult 创建结果
|
||||
*/
|
||||
createTable(tableName: string, columns: UTSJSONObject): SQLiteResult {
|
||||
const dataMap = new Map<string,string>();
|
||||
for(const key in columns){
|
||||
const item = columns.getString(key)!;
|
||||
dataMap.set(key,item)
|
||||
}
|
||||
|
||||
const columnDefinitions = Array.from(dataMap.entries())
|
||||
.map(([name, type]) => `${name} ${type}`)
|
||||
.join(',');
|
||||
const sql = `CREATE TABLE IF NOT EXISTS ${tableName} (${columnDefinitions})`;
|
||||
return this.run(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据表
|
||||
* @param tableName 要删除的表名
|
||||
* @returns SQLiteResult 删除结果
|
||||
*/
|
||||
dropTable(tableName: string): SQLiteResult {
|
||||
const sql = `DROP TABLE IF EXISTS ${tableName}`;
|
||||
return this.run(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭数据库连接
|
||||
* 释放数据库资源,关闭后需要重新创建才能使用
|
||||
*/
|
||||
close(): void {
|
||||
if (this.db) {
|
||||
this.db.close();
|
||||
this.db = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始事务
|
||||
* 开始一个新的事务,在提交或回滚之前,所有操作都在事务内
|
||||
* @returns SQLiteResult 事务开始结果
|
||||
*/
|
||||
beginTransaction(): SQLiteResult{
|
||||
if (this.isInTransaction) {
|
||||
return { error: 'Transaction already in progress' };
|
||||
}
|
||||
const result = this.run('BEGIN TRANSACTION');
|
||||
if (!result.error) {
|
||||
this.isInTransaction = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交事务
|
||||
* 提交当前事务的所有操作
|
||||
* @returns SQLiteResult 事务提交结果
|
||||
*/
|
||||
commit(): SQLiteResult {
|
||||
if (!this.isInTransaction) {
|
||||
return { error: 'No transaction in progress' };
|
||||
}
|
||||
const result = this.run('COMMIT');
|
||||
if (!result.error) {
|
||||
this.isInTransaction = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚事务
|
||||
* 撤销当前事务中的所有操作
|
||||
* @returns SQLiteResult 事务回滚结果
|
||||
*/
|
||||
rollback(): SQLiteResult {
|
||||
if (!this.isInTransaction) {
|
||||
return { error: 'No transaction in progress' };
|
||||
}
|
||||
const result = this.run('ROLLBACK');
|
||||
if (!result.error) {
|
||||
this.isInTransaction = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量执行SQL语句
|
||||
* @param statements SQL语句数组,每个元素包含sql和params
|
||||
* @returns SQLiteResult[] 每个语句的执行结果数组
|
||||
*/
|
||||
executeBatch(statements: SQLiteExecuteBatchParams[]): SQLiteResult[] {
|
||||
if (this.isInTransaction) {
|
||||
return [];
|
||||
}
|
||||
const results: SQLiteResult[] = [];
|
||||
let hasError = false;
|
||||
|
||||
// 自动使用事务包装批量操作
|
||||
const wasInTransaction = this.isInTransaction;
|
||||
if (!wasInTransaction) {
|
||||
this.beginTransaction();
|
||||
}
|
||||
|
||||
try {
|
||||
for (const stmt of statements) {
|
||||
const result = this.run(stmt.sql, stmt?.params??[]);
|
||||
results.push(result);
|
||||
if (result.error) {
|
||||
hasError = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有错误并且是我们开始的事务,则回滚
|
||||
if (hasError && !wasInTransaction) {
|
||||
this.rollback();
|
||||
} else if (!wasInTransaction) {
|
||||
// 如果是我们开始的事务且没有错误,则提交
|
||||
this.commit();
|
||||
}
|
||||
} catch (error) {
|
||||
// 发生异常且是我们开始的事务,则回滚
|
||||
if (!wasInTransaction) {
|
||||
this.rollback();
|
||||
}
|
||||
results.push({ error: error.message });
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据库文件路径
|
||||
* 获取当前数据库文件的完整路径,获取前会备份到缓存目录
|
||||
* @returns string|null 数据库文件路径,如果数据库未初始化则返回null
|
||||
*/
|
||||
getDatabasePath():Uint8Array|null{
|
||||
if (this.db==null){
|
||||
console.error('Database not initialized')
|
||||
return null
|
||||
}
|
||||
const data = this.db.export();
|
||||
// const blob = new Blob([data], { type: 'application/octet-stream' });
|
||||
// const url = window.URL.createObjectURL(blob);
|
||||
|
||||
return data;
|
||||
}
|
||||
/**
|
||||
* 当前数据库的目录
|
||||
*/
|
||||
setDefaultDirectory(directory: string){
|
||||
if (this.db==null){
|
||||
console.error('Database not initialized')
|
||||
return
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 设置数据库密码
|
||||
* 空值或者null即删除密码
|
||||
*/
|
||||
setPassword(password: string|null = null){
|
||||
if (this.db==null){
|
||||
console.error('Database not initialized')
|
||||
return
|
||||
}
|
||||
this.config.password = password
|
||||
this.config.encryption = password!=null&&!!password
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user