diff --git a/src/database/database.go b/src/database/database.go index febb5ff..294fe72 100644 --- a/src/database/database.go +++ b/src/database/database.go @@ -30,14 +30,14 @@ const ( ) type columsDefinition struct { - Name string - Type string - Default any - IsNullable bool - Extra string - Charset any - Collation any - Table string + Name string + Type string + Default any + IsNullable bool + AutoIncrement bool + Charset any + Collation any + Table string } type indexDefinition struct { @@ -63,68 +63,255 @@ type tableDefinition struct { } type databaseDefinition struct { - Name string Tables []tableDefinition Indexes []indexDefinition ForeignKeys []foreignKeyDefinition } -var listTables = []tableDefinition{ - { - Name: "users", - Columns: []columsDefinition{ +var databaseStructure databaseDefinition = databaseDefinition{ + Tables: []tableDefinition{ + { + Name: "user", + Columns: []columsDefinition{ + { + Name: "id", + Type: "int(11)", + IsNullable: false, + AutoIncrement: true, + }, + { + Name: "name", + Type: "varchar(256)", + IsNullable: false, + Charset: "utf8mb4", + Collation: "utf8mb4_unicode_ci", + }, + { + Name: "password", + Type: "varchar(256)", + IsNullable: false, + Charset: "utf8mb4", + Collation: "utf8mb4_bin", + }, + { + Name: "password_salt", + Type: "varchar(256)", + IsNullable: false, + Charset: "utf8mb4", + Collation: "utf8mb4_bin", + }, + { + Name: "permission_level", + Type: "enum('ADMIN', 'MODERATOR', 'USER')", + Default: "'USER'", + IsNullable: false, + Charset: nil, + }, + { + Name: "email", + Type: "varchar(256)", + IsNullable: true, + Default: "NULL", + Charset: "utf8mb4", + Collation: "utf8mb4_bin", + }, + }, + }, + { + Name: "folder", + Columns: []columsDefinition{ + { + Name: "id", + Type: "int(11)", + IsNullable: false, + AutoIncrement: true, + }, + { + Name: "name", + Type: "varchar(64)", + IsNullable: false, + Charset: "utf8mb4", + Collation: "utf8mb4_unicode_ci", + }, + }, + }, + { + Name: "tag", + Columns: []columsDefinition{ + { + Name: "id", + Type: "int(11)", + IsNullable: false, + AutoIncrement: true, + }, + { + Name: "name", + Type: "varchar(64)", + IsNullable: false, + Charset: "utf8mb4", + Collation: "utf8mb4_unicode_ci", + }, + }, + }, + { + Name: "website", + Columns: []columsDefinition{ + { + Name: "id", + Type: "int(11)", + IsNullable: false, + AutoIncrement: true, + }, + { + Name: "name", + Type: "varchar(256)", + IsNullable: false, + Charset: "utf8mb4", + Collation: "utf8mb4_unicode_ci", + }, + { + Name: "content", + Type: "longtext", + IsNullable: true, + Default: "NULL", + Charset: "utf8mb4", + Collation: "utf8mb4_bin", + }, + { + Name: "description", + Type: "longtext", + IsNullable: true, + Default: "NULL", + Charset: "utf8mb4", + Collation: "utf8mb4_bin", + }, + { + Name: "folder_id", + Type: "int(11)", + IsNullable: false, + }, + { + Name: "date", + Type: "datetime", + IsNullable: false, + Default: "current_timestamp()", + }, + { + Name: "title", + Type: "varchar(256)", + IsNullable: false, + Charset: "utf8mb4", + Collation: "utf8mb4_unicode_ci", + }, + }, + }, + { + Name: "tags_websites", + Columns: []columsDefinition{ + { + Name: "tag_id", + Type: "int(10)", + IsNullable: false, + }, + { + Name: "website_id", + Type: "int(10)", + IsNullable: false, + }, + { + Name: "value", + Type: "varchar(64)", + IsNullable: false, + Charset: "utf8mb4", + Collation: "utf8mb4_unicode_ci", + }, + }, + }, + }, + Indexes: []indexDefinition{ + // PRIMARY KEYS + /* { - Name: "id", - Type: "int(11)", - Default: nil, - IsNullable: false, - Extra: "AUTO_INCREMENT", + Name: "", + Type: database_ENUM_INDEX_PRIMARY, + Table: "user", + Columns: []string{ + "id", + }, }, { - Name: "name", - Type: "varchar(256)", - Default: nil, - IsNullable: false, - Extra: "", - Charset: "utf8mb4", - Collation: "utf8mb4_unicode_ci", + Name: "", + Type: database_ENUM_INDEX_PRIMARY, + Table: "folder", + Columns: []string{ + "id", + }, }, { - Name: "password", - Type: "varchar(256)", - Default: nil, - IsNullable: false, - Extra: "", - Charset: "utf8mb4", - Collation: "utf8mb4_unicode", + Name: "", + Type: database_ENUM_INDEX_PRIMARY, + Table: "tag", + Columns: []string{ + "id", + }, }, { - Name: "password-salt", - Type: "varchar(256)", - Default: nil, - IsNullable: false, - Extra: "", - Charset: "utf8mb4", - Collation: "utf8mb4_unicode", + Name: "", + Type: database_ENUM_INDEX_PRIMARY, + Table: "website", + Columns: []string{ + "id", + }, }, { - Name: "permission_level", - Type: "enum('ADMIN', 'MODERATOR', 'USER')", - Default: "USER", - IsNullable: false, - Extra: "", - Charset: nil, + Name: "", + Type: database_ENUM_INDEX_PRIMARY, + Table: "tags_websites", + Columns: []string{ + "tag_id", + "website_id", + }, }, - { - Name: "email", - Type: "varchar(256)", - Default: nil, - IsNullable: true, - Extra: "", - Charset: "utf8mb4", - Collation: "utf8mb4_unicode", + */ + // UNIQUE KEYS + + { + Name: "UNIQUE_user_name", + Type: database_ENUM_INDEX_UNIQUE, + Table: "user", + Columns: []string{ + "name", }, }, + { + Name: "UNIQUE_website_name_folder", + Type: database_ENUM_INDEX_UNIQUE, + Table: "website", + Columns: []string{ + "name", + "folder_id", + }, + }, + }, + ForeignKeys: []foreignKeyDefinition{ + { + Name: "FK_tags_websites__tag_id", + Table: "tags_websites", + ColumnName: "tag_id", + PointingToTable: "tag", + PointingToColumn: "id", + UpdateRule: "CASCADE", + DeleteRule: "CASCADE", + }, + { + Name: "FK_tags_websites__website_id", + Table: "tags_websites", + ColumnName: "website_id", + PointingToTable: "website", + PointingToColumn: "id", + UpdateRule: "CASCADE", + DeleteRule: "CASCADE", + }, }, } @@ -146,6 +333,8 @@ func Init() { profiler.Register(profiler_DATABASE_QUERY, 4000, "µs") isInit = true + destroyDatabase() + updateDatabaseStructure() initDatabaseStructure() } @@ -247,13 +436,47 @@ func executeQuery(query string, args ...any) *sql.Rows { profiler.Add(profiler_DATABASE_QUERY, time.Duration(time.Since(tStart).Microseconds())) if err != nil { - log.Printf("Cannot query the database ! (Query : %s) with args ", query) - log.Fatalln(args...) + log.Printf("Cannot query the database ! Query : %s", query) + log.Fatalln("With args :", args) } return rows } +func executeExec(query string, args ...any) sql.Result { + if !isInit { + return nil + } + if db == nil { + connect() + } + + var err error + var result sql.Result + tStart := time.Now() + for i := range dbConfig.RetriesOnError { + result, err = db.Exec(query, args...) + if err == nil { + break + } + log.Printf("Error while executing on the database [%d/%d] : %s\n", i+1, dbConfig.RetriesOnError, err) + if i+1 == dbConfig.RetriesOnError { + break + } + time.Sleep((time.Duration)(dbConfig.TimeBetweenRetriesMs) * time.Millisecond) + ping() + } + profiler.Add(profiler_DATABASE_QUERY, time.Duration(time.Since(tStart).Microseconds())) + + if err != nil { + log.Printf("Cannot execute on the database ! Command : %s", query) + log.Fatalln("With args : ", args) + } + + return result + +} + func fetchStoredRoutines() { storedRoutines = nil var rows *sql.Rows = executeQuery("SELECT ROUTINE_NAME, ROUTINE_TYPE FROM information_schema.ROUTINES WHERE ROUTINE_SCHEMA=?;", dbConfig.Database) @@ -318,7 +541,8 @@ func callStoredFunction(name string, args ...any) { } func fetchTables() []string { - var rows *sql.Rows = executeQuery("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA=?;", dbConfig.Database) + var rows *sql.Rows = executeQuery("SHOW TABLES;") + //var rows *sql.Rows = executeQuery("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA=?;", dbConfig.Database) var tables []string = make([]string, 0) if rows != nil { defer rows.Close() @@ -332,19 +556,18 @@ func fetchTables() []string { } func fetchColumns(table string) []columsDefinition { - var rows *sql.Rows = executeQuery("SELECT COLUMN_NAME, COLUMN_TYPE, COLUMN_DEFAULT, IS_NULLABLE, EXTRA, CHARACTER_SET_NAME, COLLATION_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=? AND TABLE_NAME=?;", dbConfig.Database, table) + var rows *sql.Rows = executeQuery("SELECT COLUMN_NAME, COLUMN_TYPE, COLUMN_DEFAULT, IS_NULLABLE, CHARACTER_SET_NAME, COLLATION_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=? AND TABLE_NAME=?;", dbConfig.Database, table) var columns []columsDefinition = make([]columsDefinition, 0) if rows != nil { defer rows.Close() for rows.Next() { - var resName, resType, resDefault, resNullable, resExtra, resCharset, resCollation string - rows.Scan(&resName, &resType, &resDefault, &resNullable, &resExtra, &resCharset, &resCollation) + var resName, resType, resDefault, resNullable, resCharset, resCollation string + rows.Scan(&resName, &resType, &resDefault, &resNullable, &resCharset, &resCollation) columns = append(columns, columsDefinition{ Name: resName, Type: resType, Default: resDefault, IsNullable: resNullable == "YES", - Extra: resExtra, Charset: resCharset, Collation: resCollation, Table: table, @@ -421,8 +644,8 @@ func initDatabaseStructure() { var columns []columsDefinition = fetchColumns(tables[i]) log.Printf(" - Table %s : \n", tables[i]) for j := range columns { - log.Printf(" - Column %s %s IsNullable:%t Default:%s Extra:%s Charset:%s/%s\n", - columns[j].Name, columns[j].Type, columns[j].IsNullable, columns[j].Default, columns[j].Extra, columns[j].Charset, columns[j].Collation) + log.Printf(" - Column %s %s IsNullable:%t Default:%s Charset:%s/%s\n", + columns[j].Name, columns[j].Type, columns[j].IsNullable, columns[j].Default, columns[j].Charset, columns[j].Collation) } } @@ -438,3 +661,119 @@ func initDatabaseStructure() { log.Printf(" - Foreign key %s\n", foreignKeys[i]) } } + +func updateDatabaseStructure() { + var tablesNeeded []tableDefinition = databaseStructure.Tables + + for t := range tablesNeeded { + var table tableDefinition = tablesNeeded[t] + var columnsNeeded []columsDefinition = table.Columns + + executeExec(fmt.Sprintf("DROP TABLE IF EXISTS %s;", table.Name)) + var query string = fmt.Sprintf("CREATE TABLE %s (\n", table.Name) + for c := range columnsNeeded { + if c != 0 { + query += ",\n" + } + var column columsDefinition = columnsNeeded[c] + query += fmt.Sprintf("%s %s", column.Name, column.Type) + + str, ok := column.Charset.(string) + if ok { + query += fmt.Sprintf(" CHARACTER SET %s", str) + } + + str, ok = column.Collation.(string) + if ok { + query += fmt.Sprintf(" COLLATE %s", str) + } + + if !column.IsNullable { + query += " NOT NULL" + } + + str, ok = column.Default.(string) + if ok { + query += fmt.Sprintf(" DEFAULT %s", str) + } + } + + query += "\n);" + fmt.Println(query) + executeExec(query) + } + + var indexesNeeded []indexDefinition = databaseStructure.Indexes + + for i := range indexesNeeded { + var index indexDefinition = indexesNeeded[i] + // ALTER TABLE