From da6ae8d7d480750d10af3e1bd96c4e34d9aa7bc4 Mon Sep 17 00:00:00 2001 From: Azareal Date: Mon, 16 Oct 2017 08:32:58 +0100 Subject: [PATCH] Gosora now supports MSSQL (Microsoft SQL Server). Fixed a bug with MemoryUserStore's length counter. The upsert API is currently a confusing mess, we'll have it all fixed up soon. Added the Delete method to the User struct. Improved the test coverage for the user subsystem. --- gen_mssql.go | 244 ++-- gen_mysql.go | 28 +- gen_tables.go | 15 + general_test.go | 43 +- install/install/mssql.go | 7 + member_routes.go | 2 +- misc_test.go | 290 +++-- mssql.go | 60 +- plugin_test.go | 4 +- public/global.js | 57 +- query_gen/lib/builder.go | 11 +- query_gen/lib/install.go | 44 +- query_gen/lib/mssql.go | 1130 +++++++++++++++++ query_gen/lib/mysql.go | 63 +- query_gen/lib/pgsql.go | 17 + query_gen/lib/querygen.go | 36 +- query_gen/lib/utils.go | 7 +- query_gen/main.go | 515 ++------ query_gen/spitter.go | 44 + query_gen/tables.go | 363 ++++++ routes.go | 6 +- schema/mssql/query_activity_stream.sql | 1 - .../mssql/query_activity_stream_matches.sql | 1 - schema/mssql/query_activity_subscriptions.sql | 1 - schema/mssql/query_administration_logs.sql | 1 - schema/mssql/query_attachments.sql | 1 - schema/mssql/query_emails.sql | 1 - schema/mssql/query_forums.sql | 1 - schema/mssql/query_forums_permissions.sql | 1 - schema/mssql/query_likes.sql | 1 - schema/mssql/query_moderation_logs.sql | 1 - schema/mssql/query_plugins.sql | 1 - schema/mssql/query_replies.sql | 5 +- schema/mssql/query_revisions.sql | 1 - schema/mssql/query_settings.sql | 1 - schema/mssql/query_sync.sql | 1 - schema/mssql/query_themes.sql | 1 - schema/mssql/query_topics.sql | 1 - schema/mssql/query_users.sql | 1 - schema/mssql/query_users_groups.sql | 1 - schema/mssql/query_users_groups_scheduler.sql | 1 - schema/mssql/query_users_replies.sql | 1 - schema/mssql/query_widgets.sql | 1 - schema/mssql/query_word_filters.sql | 1 - schema/mysql/query_replies.sql | 4 +- schema/pgsql/query_replies.sql | 4 +- tasks.go | 12 +- template_init.go | 2 +- topic.go | 38 +- user.go | 24 +- user_store.go | 30 +- 51 files changed, 2357 insertions(+), 770 deletions(-) create mode 100644 gen_tables.go create mode 100644 query_gen/lib/mssql.go create mode 100644 query_gen/spitter.go create mode 100644 query_gen/tables.go diff --git a/gen_mssql.go b/gen_mssql.go index 27e7199a..5488a676 100644 --- a/gen_mssql.go +++ b/gen_mssql.go @@ -113,6 +113,7 @@ var verifyEmailStmt *sql.Stmt var setTempGroupStmt *sql.Stmt var updateWordFilterStmt *sql.Stmt var bumpSyncStmt *sql.Stmt +var deleteUserStmt *sql.Stmt var deleteReplyStmt *sql.Stmt var deleteProfileReplyStmt *sql.Stmt var deleteForumPermsByForumStmt *sql.Stmt @@ -245,9 +246,9 @@ func _gen_mssql() (err error) { } log.Print("Preparing getUsersOffset statement.") - getUsersOffsetStmt, err = db.Prepare("SELECT [uid],[name],[group],[active],[is_super_admin],[avatar] FROM [users] OFFSET ?1 ROWS FETCH NEXT ?2 ROWS ONLY") + getUsersOffsetStmt, err = db.Prepare("SELECT [uid],[name],[group],[active],[is_super_admin],[avatar] FROM [users] ORDER BY uid ASC OFFSET ?1 ROWS FETCH NEXT ?2 ROWS ONLY") if err != nil { - log.Print("Bad Query: ","SELECT [uid],[name],[group],[active],[is_super_admin],[avatar] FROM [users] OFFSET ?1 ROWS FETCH NEXT ?2 ROWS ONLY") + log.Print("Bad Query: ","SELECT [uid],[name],[group],[active],[is_super_admin],[avatar] FROM [users] ORDER BY uid ASC OFFSET ?1 ROWS FETCH NEXT ?2 ROWS ONLY") return err } @@ -273,9 +274,9 @@ func _gen_mssql() (err error) { } log.Print("Preparing getModlogsOffset statement.") - getModlogsOffsetStmt, err = db.Prepare("SELECT [action],[elementID],[elementType],[ipaddress],[actorID],[doneAt] FROM [moderation_logs] OFFSET ?1 ROWS FETCH NEXT ?2 ROWS ONLY") + getModlogsOffsetStmt, err = db.Prepare("SELECT [action],[elementID],[elementType],[ipaddress],[actorID],[doneAt] FROM [moderation_logs] ORDER BY doneAt DESC OFFSET ?1 ROWS FETCH NEXT ?2 ROWS ONLY") if err != nil { - log.Print("Bad Query: ","SELECT [action],[elementID],[elementType],[ipaddress],[actorID],[doneAt] FROM [moderation_logs] OFFSET ?1 ROWS FETCH NEXT ?2 ROWS ONLY") + log.Print("Bad Query: ","SELECT [action],[elementID],[elementType],[ipaddress],[actorID],[doneAt] FROM [moderation_logs] ORDER BY doneAt DESC OFFSET ?1 ROWS FETCH NEXT ?2 ROWS ONLY") return err } @@ -385,9 +386,9 @@ func _gen_mssql() (err error) { } log.Print("Preparing getTopicRepliesOffset statement.") - getTopicRepliesOffsetStmt, err = db.Prepare("SELECT [replies].[rid],[replies].[content],[replies].[createdBy],[replies].[createdAt],[replies].[lastEdit],[replies].[lastEditBy],[users].[avatar],[users].[name],[users].[group],[users].[url_prefix],[users].[url_name],[users].[level],[replies].[ipaddress],[replies].[likeCount],[replies].[actionType] FROM [replies] LEFT JOIN [users] ON [replies].[createdBy] = [users].[uid] WHERE [replies].[tid] = ?1 OFFSET ?2 ROWS FETCH NEXT ?3 ROWS ONLY") + getTopicRepliesOffsetStmt, err = db.Prepare("SELECT [replies].[rid],[replies].[content],[replies].[createdBy],[replies].[createdAt],[replies].[lastEdit],[replies].[lastEditBy],[users].[avatar],[users].[name],[users].[group],[users].[url_prefix],[users].[url_name],[users].[level],[replies].[ipaddress],[replies].[likeCount],[replies].[actionType] FROM [replies] LEFT JOIN [users] ON [replies].[createdBy] = [users].[uid] WHERE [replies].[tid] = ?1 ORDER BY replies.rid ASC OFFSET ?2 ROWS FETCH NEXT ?3 ROWS ONLY") if err != nil { - log.Print("Bad Query: ","SELECT [replies].[rid],[replies].[content],[replies].[createdBy],[replies].[createdAt],[replies].[lastEdit],[replies].[lastEditBy],[users].[avatar],[users].[name],[users].[group],[users].[url_prefix],[users].[url_name],[users].[level],[replies].[ipaddress],[replies].[likeCount],[replies].[actionType] FROM [replies] LEFT JOIN [users] ON [replies].[createdBy] = [users].[uid] WHERE [replies].[tid] = ?1 OFFSET ?2 ROWS FETCH NEXT ?3 ROWS ONLY") + log.Print("Bad Query: ","SELECT [replies].[rid],[replies].[content],[replies].[createdBy],[replies].[createdAt],[replies].[lastEdit],[replies].[lastEditBy],[users].[avatar],[users].[name],[users].[group],[users].[url_prefix],[users].[url_name],[users].[level],[replies].[ipaddress],[replies].[likeCount],[replies].[actionType] FROM [replies] LEFT JOIN [users] ON [replies].[createdBy] = [users].[uid] WHERE [replies].[tid] = ?1 ORDER BY replies.rid ASC OFFSET ?2 ROWS FETCH NEXT ?3 ROWS ONLY") return err } @@ -434,9 +435,9 @@ func _gen_mssql() (err error) { } log.Print("Preparing getWatchers statement.") - getWatchersStmt, err = db.Prepare("") + getWatchersStmt, err = db.Prepare("SELECT [activity_subscriptions].[user] FROM [activity_stream] INNER JOIN [activity_subscriptions] ON [activity_subscriptions].[targetType] = [activity_stream].[elementType] AND [activity_subscriptions].[targetID] = [activity_stream].[elementID] AND [activity_subscriptions].[user] != [activity_stream].[actor] WHERE [asid] = ?1") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","SELECT [activity_subscriptions].[user] FROM [activity_stream] INNER JOIN [activity_subscriptions] ON [activity_subscriptions].[targetType] = [activity_stream].[elementType] AND [activity_subscriptions].[targetID] = [activity_stream].[elementID] AND [activity_subscriptions].[user] != [activity_stream].[actor] WHERE [asid] = ?1") return err } @@ -448,23 +449,23 @@ func _gen_mssql() (err error) { } log.Print("Preparing createReport statement.") - createReportStmt, err = db.Prepare("INSERT INTO [topics] ([title],[content],[parsed_content],[createdAt],[lastReplyAt],[createdBy],[data],[parentID],[css_class]) VALUES (?,?,?,GETUTCDATE(),GETUTCDATE(),?,?,1,'report')") + createReportStmt, err = db.Prepare("INSERT INTO [topics] ([title],[content],[parsed_content],[createdAt],[lastReplyAt],[createdBy],[lastReplyBy],[data],[parentID],[css_class]) VALUES (?,?,?,GETUTCDATE(),GETUTCDATE(),?,?,?,1,'report')") if err != nil { - log.Print("Bad Query: ","INSERT INTO [topics] ([title],[content],[parsed_content],[createdAt],[lastReplyAt],[createdBy],[data],[parentID],[css_class]) VALUES (?,?,?,GETUTCDATE(),GETUTCDATE(),?,?,1,'report')") + log.Print("Bad Query: ","INSERT INTO [topics] ([title],[content],[parsed_content],[createdAt],[lastReplyAt],[createdBy],[lastReplyBy],[data],[parentID],[css_class]) VALUES (?,?,?,GETUTCDATE(),GETUTCDATE(),?,?,?,1,'report')") return err } log.Print("Preparing createReply statement.") - createReplyStmt, err = db.Prepare("INSERT INTO [replies] ([tid],[content],[parsed_content],[createdAt],[ipaddress],[words],[createdBy]) VALUES (?,?,?,GETUTCDATE(),?,?,?)") + createReplyStmt, err = db.Prepare("INSERT INTO [replies] ([tid],[content],[parsed_content],[createdAt],[lastUpdated],[ipaddress],[words],[createdBy]) VALUES (?,?,?,GETUTCDATE(),GETUTCDATE(),?,?,?)") if err != nil { - log.Print("Bad Query: ","INSERT INTO [replies] ([tid],[content],[parsed_content],[createdAt],[ipaddress],[words],[createdBy]) VALUES (?,?,?,GETUTCDATE(),?,?,?)") + log.Print("Bad Query: ","INSERT INTO [replies] ([tid],[content],[parsed_content],[createdAt],[lastUpdated],[ipaddress],[words],[createdBy]) VALUES (?,?,?,GETUTCDATE(),GETUTCDATE(),?,?,?)") return err } log.Print("Preparing createActionReply statement.") - createActionReplyStmt, err = db.Prepare("INSERT INTO [replies] ([tid],[actionType],[ipaddress],[createdBy]) VALUES (?,?,?,?)") + createActionReplyStmt, err = db.Prepare("INSERT INTO [replies] ([tid],[actionType],[ipaddress],[createdBy],[createdAt],[lastUpdated],[content],[parsed_content]) VALUES (?,?,?,?,GETUTCDATE(),GETUTCDATE(),'','')") if err != nil { - log.Print("Bad Query: ","INSERT INTO [replies] ([tid],[actionType],[ipaddress],[createdBy]) VALUES (?,?,?,?)") + log.Print("Bad Query: ","INSERT INTO [replies] ([tid],[actionType],[ipaddress],[createdBy],[createdAt],[lastUpdated],[content],[parsed_content]) VALUES (?,?,?,?,GETUTCDATE(),GETUTCDATE(),'','')") return err } @@ -574,338 +575,345 @@ func _gen_mssql() (err error) { } log.Print("Preparing addForumPermsToGroup statement.") - addForumPermsToGroupStmt, err = db.Prepare("") + addForumPermsToGroupStmt, err = db.Prepare("MERGE [forums_permissions] WITH(HOLDLOCK) as t1 USING (VALUES(?,?,?,?)) AS updates (f0,f1,f2,f3) ON [gid] = ? [fid] = ? WHEN MATCHED THEN UPDATE SET [gid] = f0,[fid] = f1,[preset] = f2,[permissions] = f3 WHEN NOT MATCHED THEN INSERT([gid],[fid],[preset],[permissions]) VALUES (f0,f1,f2,f3);") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","MERGE [forums_permissions] WITH(HOLDLOCK) as t1 USING (VALUES(?,?,?,?)) AS updates (f0,f1,f2,f3) ON [gid] = ? [fid] = ? WHEN MATCHED THEN UPDATE SET [gid] = f0,[fid] = f1,[preset] = f2,[permissions] = f3 WHEN NOT MATCHED THEN INSERT([gid],[fid],[preset],[permissions]) VALUES (f0,f1,f2,f3);") return err } log.Print("Preparing replaceScheduleGroup statement.") - replaceScheduleGroupStmt, err = db.Prepare("") + replaceScheduleGroupStmt, err = db.Prepare("MERGE [users_groups_scheduler] WITH(HOLDLOCK) as t1 USING (VALUES(?,?,?,GETUTCDATE(),?,?)) AS updates (f0,f1,f2,f3,f4,f5) ON [uid] = ? WHEN MATCHED THEN UPDATE SET [uid] = f0,[set_group] = f1,[issued_by] = f2,[issued_at] = f3,[revert_at] = f4,[temporary] = f5 WHEN NOT MATCHED THEN INSERT([uid],[set_group],[issued_by],[issued_at],[revert_at],[temporary]) VALUES (f0,f1,f2,f3,f4,f5);") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","MERGE [users_groups_scheduler] WITH(HOLDLOCK) as t1 USING (VALUES(?,?,?,GETUTCDATE(),?,?)) AS updates (f0,f1,f2,f3,f4,f5) ON [uid] = ? WHEN MATCHED THEN UPDATE SET [uid] = f0,[set_group] = f1,[issued_by] = f2,[issued_at] = f3,[revert_at] = f4,[temporary] = f5 WHEN NOT MATCHED THEN INSERT([uid],[set_group],[issued_by],[issued_at],[revert_at],[temporary]) VALUES (f0,f1,f2,f3,f4,f5);") return err } log.Print("Preparing addRepliesToTopic statement.") - addRepliesToTopicStmt, err = db.Prepare("") + addRepliesToTopicStmt, err = db.Prepare("UPDATE [topics] SET [postCount] = [postCount] + ?,[lastReplyBy] = ?,[lastReplyAt] = GETUTCDATE() WHERE [tid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [topics] SET [postCount] = [postCount] + ?,[lastReplyBy] = ?,[lastReplyAt] = GETUTCDATE() WHERE [tid] = ?") return err } log.Print("Preparing removeRepliesFromTopic statement.") - removeRepliesFromTopicStmt, err = db.Prepare("") + removeRepliesFromTopicStmt, err = db.Prepare("UPDATE [topics] SET [postCount] = [postCount] - ? WHERE [tid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [topics] SET [postCount] = [postCount] - ? WHERE [tid] = ?") return err } log.Print("Preparing addTopicsToForum statement.") - addTopicsToForumStmt, err = db.Prepare("") + addTopicsToForumStmt, err = db.Prepare("UPDATE [forums] SET [topicCount] = [topicCount] + ? WHERE [fid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [forums] SET [topicCount] = [topicCount] + ? WHERE [fid] = ?") return err } log.Print("Preparing removeTopicsFromForum statement.") - removeTopicsFromForumStmt, err = db.Prepare("") + removeTopicsFromForumStmt, err = db.Prepare("UPDATE [forums] SET [topicCount] = [topicCount] - ? WHERE [fid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [forums] SET [topicCount] = [topicCount] - ? WHERE [fid] = ?") return err } log.Print("Preparing updateForumCache statement.") - updateForumCacheStmt, err = db.Prepare("") + updateForumCacheStmt, err = db.Prepare("UPDATE [forums] SET [lastTopicID] = ?,[lastReplyerID] = ? WHERE [fid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [forums] SET [lastTopicID] = ?,[lastReplyerID] = ? WHERE [fid] = ?") return err } log.Print("Preparing addLikesToTopic statement.") - addLikesToTopicStmt, err = db.Prepare("") + addLikesToTopicStmt, err = db.Prepare("UPDATE [topics] SET [likeCount] = [likeCount] + ? WHERE [tid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [topics] SET [likeCount] = [likeCount] + ? WHERE [tid] = ?") return err } log.Print("Preparing addLikesToReply statement.") - addLikesToReplyStmt, err = db.Prepare("") + addLikesToReplyStmt, err = db.Prepare("UPDATE [replies] SET [likeCount] = [likeCount] + ? WHERE [rid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [replies] SET [likeCount] = [likeCount] + ? WHERE [rid] = ?") return err } log.Print("Preparing editTopic statement.") - editTopicStmt, err = db.Prepare("") + editTopicStmt, err = db.Prepare("UPDATE [topics] SET [title] = ?,[content] = ?,[parsed_content] = ? WHERE [tid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [topics] SET [title] = ?,[content] = ?,[parsed_content] = ? WHERE [tid] = ?") return err } log.Print("Preparing editReply statement.") - editReplyStmt, err = db.Prepare("") + editReplyStmt, err = db.Prepare("UPDATE [replies] SET [content] = ?,[parsed_content] = ? WHERE [rid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [replies] SET [content] = ?,[parsed_content] = ? WHERE [rid] = ?") return err } log.Print("Preparing stickTopic statement.") - stickTopicStmt, err = db.Prepare("") + stickTopicStmt, err = db.Prepare("UPDATE [topics] SET [sticky] = 1 WHERE [tid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [topics] SET [sticky] = 1 WHERE [tid] = ?") return err } log.Print("Preparing unstickTopic statement.") - unstickTopicStmt, err = db.Prepare("") + unstickTopicStmt, err = db.Prepare("UPDATE [topics] SET [sticky] = 0 WHERE [tid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [topics] SET [sticky] = 0 WHERE [tid] = ?") return err } log.Print("Preparing lockTopic statement.") - lockTopicStmt, err = db.Prepare("") + lockTopicStmt, err = db.Prepare("UPDATE [topics] SET [is_closed] = 1 WHERE [tid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [topics] SET [is_closed] = 1 WHERE [tid] = ?") return err } log.Print("Preparing unlockTopic statement.") - unlockTopicStmt, err = db.Prepare("") + unlockTopicStmt, err = db.Prepare("UPDATE [topics] SET [is_closed] = 0 WHERE [tid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [topics] SET [is_closed] = 0 WHERE [tid] = ?") return err } log.Print("Preparing updateLastIP statement.") - updateLastIPStmt, err = db.Prepare("") + updateLastIPStmt, err = db.Prepare("UPDATE [users] SET [last_ip] = ? WHERE [uid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [users] SET [last_ip] = ? WHERE [uid] = ?") return err } log.Print("Preparing updateSession statement.") - updateSessionStmt, err = db.Prepare("") + updateSessionStmt, err = db.Prepare("UPDATE [users] SET [session] = ? WHERE [uid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [users] SET [session] = ? WHERE [uid] = ?") return err } log.Print("Preparing setPassword statement.") - setPasswordStmt, err = db.Prepare("") + setPasswordStmt, err = db.Prepare("UPDATE [users] SET [password] = ?,[salt] = ? WHERE [uid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [users] SET [password] = ?,[salt] = ? WHERE [uid] = ?") return err } log.Print("Preparing setAvatar statement.") - setAvatarStmt, err = db.Prepare("") + setAvatarStmt, err = db.Prepare("UPDATE [users] SET [avatar] = ? WHERE [uid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [users] SET [avatar] = ? WHERE [uid] = ?") return err } log.Print("Preparing setUsername statement.") - setUsernameStmt, err = db.Prepare("") + setUsernameStmt, err = db.Prepare("UPDATE [users] SET [name] = ? WHERE [uid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [users] SET [name] = ? WHERE [uid] = ?") return err } log.Print("Preparing changeGroup statement.") - changeGroupStmt, err = db.Prepare("") + changeGroupStmt, err = db.Prepare("UPDATE [users] SET [group] = ? WHERE [uid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [users] SET [group] = ? WHERE [uid] = ?") return err } log.Print("Preparing activateUser statement.") - activateUserStmt, err = db.Prepare("") + activateUserStmt, err = db.Prepare("UPDATE [users] SET [active] = 1 WHERE [uid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [users] SET [active] = 1 WHERE [uid] = ?") return err } log.Print("Preparing updateUserLevel statement.") - updateUserLevelStmt, err = db.Prepare("") + updateUserLevelStmt, err = db.Prepare("UPDATE [users] SET [level] = ? WHERE [uid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [users] SET [level] = ? WHERE [uid] = ?") return err } log.Print("Preparing incrementUserScore statement.") - incrementUserScoreStmt, err = db.Prepare("") + incrementUserScoreStmt, err = db.Prepare("UPDATE [users] SET [score] = [score] + ? WHERE [uid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [users] SET [score] = [score] + ? WHERE [uid] = ?") return err } log.Print("Preparing incrementUserPosts statement.") - incrementUserPostsStmt, err = db.Prepare("") + incrementUserPostsStmt, err = db.Prepare("UPDATE [users] SET [posts] = [posts] + ? WHERE [uid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [users] SET [posts] = [posts] + ? WHERE [uid] = ?") return err } log.Print("Preparing incrementUserBigposts statement.") - incrementUserBigpostsStmt, err = db.Prepare("") + incrementUserBigpostsStmt, err = db.Prepare("UPDATE [users] SET [posts] = [posts] + ?,[bigposts] = [bigposts] + ? WHERE [uid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [users] SET [posts] = [posts] + ?,[bigposts] = [bigposts] + ? WHERE [uid] = ?") return err } log.Print("Preparing incrementUserMegaposts statement.") - incrementUserMegapostsStmt, err = db.Prepare("") + incrementUserMegapostsStmt, err = db.Prepare("UPDATE [users] SET [posts] = [posts] + ?,[bigposts] = [bigposts] + ?,[megaposts] = [megaposts] + ? WHERE [uid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [users] SET [posts] = [posts] + ?,[bigposts] = [bigposts] + ?,[megaposts] = [megaposts] + ? WHERE [uid] = ?") return err } log.Print("Preparing incrementUserTopics statement.") - incrementUserTopicsStmt, err = db.Prepare("") + incrementUserTopicsStmt, err = db.Prepare("UPDATE [users] SET [topics] = [topics] + ? WHERE [uid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [users] SET [topics] = [topics] + ? WHERE [uid] = ?") return err } log.Print("Preparing editProfileReply statement.") - editProfileReplyStmt, err = db.Prepare("") + editProfileReplyStmt, err = db.Prepare("UPDATE [users_replies] SET [content] = ?,[parsed_content] = ? WHERE [rid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [users_replies] SET [content] = ?,[parsed_content] = ? WHERE [rid] = ?") return err } log.Print("Preparing updateForum statement.") - updateForumStmt, err = db.Prepare("") + updateForumStmt, err = db.Prepare("UPDATE [forums] SET [name] = ?,[desc] = ?,[active] = ?,[preset] = ? WHERE [fid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [forums] SET [name] = ?,[desc] = ?,[active] = ?,[preset] = ? WHERE [fid] = ?") return err } log.Print("Preparing updateSetting statement.") - updateSettingStmt, err = db.Prepare("") + updateSettingStmt, err = db.Prepare("UPDATE [settings] SET [content] = ? WHERE [name] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [settings] SET [content] = ? WHERE [name] = ?") return err } log.Print("Preparing updatePlugin statement.") - updatePluginStmt, err = db.Prepare("") + updatePluginStmt, err = db.Prepare("UPDATE [plugins] SET [active] = ? WHERE [uname] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [plugins] SET [active] = ? WHERE [uname] = ?") return err } log.Print("Preparing updatePluginInstall statement.") - updatePluginInstallStmt, err = db.Prepare("") + updatePluginInstallStmt, err = db.Prepare("UPDATE [plugins] SET [installed] = ? WHERE [uname] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [plugins] SET [installed] = ? WHERE [uname] = ?") return err } log.Print("Preparing updateTheme statement.") - updateThemeStmt, err = db.Prepare("") + updateThemeStmt, err = db.Prepare("UPDATE [themes] SET [default] = ? WHERE [uname] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [themes] SET [default] = ? WHERE [uname] = ?") return err } log.Print("Preparing updateUser statement.") - updateUserStmt, err = db.Prepare("") + updateUserStmt, err = db.Prepare("UPDATE [users] SET [name] = ?,[email] = ?,[group] = ? WHERE [uid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [users] SET [name] = ?,[email] = ?,[group] = ? WHERE [uid] = ?") return err } log.Print("Preparing updateGroupPerms statement.") - updateGroupPermsStmt, err = db.Prepare("") + updateGroupPermsStmt, err = db.Prepare("UPDATE [users_groups] SET [permissions] = ? WHERE [gid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [users_groups] SET [permissions] = ? WHERE [gid] = ?") return err } log.Print("Preparing updateGroupRank statement.") - updateGroupRankStmt, err = db.Prepare("") + updateGroupRankStmt, err = db.Prepare("UPDATE [users_groups] SET [is_admin] = ?,[is_mod] = ?,[is_banned] = ? WHERE [gid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [users_groups] SET [is_admin] = ?,[is_mod] = ?,[is_banned] = ? WHERE [gid] = ?") return err } log.Print("Preparing updateGroup statement.") - updateGroupStmt, err = db.Prepare("") + updateGroupStmt, err = db.Prepare("UPDATE [users_groups] SET [name] = ?,[tag] = ? WHERE [gid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [users_groups] SET [name] = ?,[tag] = ? WHERE [gid] = ?") return err } log.Print("Preparing updateEmail statement.") - updateEmailStmt, err = db.Prepare("") + updateEmailStmt, err = db.Prepare("UPDATE [emails] SET [email] = ?,[uid] = ?,[validated] = ?,[token] = ? WHERE [email] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [emails] SET [email] = ?,[uid] = ?,[validated] = ?,[token] = ? WHERE [email] = ?") return err } log.Print("Preparing verifyEmail statement.") - verifyEmailStmt, err = db.Prepare("") + verifyEmailStmt, err = db.Prepare("UPDATE [emails] SET [validated] = 1,[token] = '' WHERE [email] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [emails] SET [validated] = 1,[token] = '' WHERE [email] = ?") return err } log.Print("Preparing setTempGroup statement.") - setTempGroupStmt, err = db.Prepare("") + setTempGroupStmt, err = db.Prepare("UPDATE [users] SET [temp_group] = ? WHERE [uid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [users] SET [temp_group] = ? WHERE [uid] = ?") return err } log.Print("Preparing updateWordFilter statement.") - updateWordFilterStmt, err = db.Prepare("") + updateWordFilterStmt, err = db.Prepare("UPDATE [word_filters] SET [find] = ?,[replacement] = ? WHERE [wfid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [word_filters] SET [find] = ?,[replacement] = ? WHERE [wfid] = ?") return err } log.Print("Preparing bumpSync statement.") - bumpSyncStmt, err = db.Prepare("") + bumpSyncStmt, err = db.Prepare("UPDATE [sync] SET [last_update] = GETUTCDATE()") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","UPDATE [sync] SET [last_update] = GETUTCDATE()") + return err + } + + log.Print("Preparing deleteUser statement.") + deleteUserStmt, err = db.Prepare("DELETE FROM [users] WHERE [uid] = ?") + if err != nil { + log.Print("Bad Query: ","DELETE FROM [users] WHERE [uid] = ?") return err } log.Print("Preparing deleteReply statement.") - deleteReplyStmt, err = db.Prepare("") + deleteReplyStmt, err = db.Prepare("DELETE FROM [replies] WHERE [rid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","DELETE FROM [replies] WHERE [rid] = ?") return err } log.Print("Preparing deleteProfileReply statement.") - deleteProfileReplyStmt, err = db.Prepare("") + deleteProfileReplyStmt, err = db.Prepare("DELETE FROM [users_replies] WHERE [rid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","DELETE FROM [users_replies] WHERE [rid] = ?") return err } log.Print("Preparing deleteForumPermsByForum statement.") - deleteForumPermsByForumStmt, err = db.Prepare("") + deleteForumPermsByForumStmt, err = db.Prepare("DELETE FROM [forums_permissions] WHERE [fid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","DELETE FROM [forums_permissions] WHERE [fid] = ?") return err } log.Print("Preparing deleteActivityStreamMatch statement.") - deleteActivityStreamMatchStmt, err = db.Prepare("") + deleteActivityStreamMatchStmt, err = db.Prepare("DELETE FROM [activity_stream_matches] WHERE [watcher] = ? AND [asid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","DELETE FROM [activity_stream_matches] WHERE [watcher] = ? AND [asid] = ?") return err } log.Print("Preparing deleteWordFilter statement.") - deleteWordFilterStmt, err = db.Prepare("") + deleteWordFilterStmt, err = db.Prepare("DELETE FROM [word_filters] WHERE [wfid] = ?") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","DELETE FROM [word_filters] WHERE [wfid] = ?") return err } @@ -931,30 +939,30 @@ func _gen_mssql() (err error) { } log.Print("Preparing addForumPermsToForumAdmins statement.") - addForumPermsToForumAdminsStmt, err = db.Prepare("") + addForumPermsToForumAdminsStmt, err = db.Prepare("INSERT INTO [forums_permissions] ([gid],[fid],[preset],[permissions]) SELECT [gid],[? AS fid],[? AS preset],[? AS permissions] FROM [users_groups] WHERE [is_admin] = 1") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","INSERT INTO [forums_permissions] ([gid],[fid],[preset],[permissions]) SELECT [gid],[? AS fid],[? AS preset],[? AS permissions] FROM [users_groups] WHERE [is_admin] = 1") return err } log.Print("Preparing addForumPermsToForumStaff statement.") - addForumPermsToForumStaffStmt, err = db.Prepare("") + addForumPermsToForumStaffStmt, err = db.Prepare("INSERT INTO [forums_permissions] ([gid],[fid],[preset],[permissions]) SELECT [gid],[? AS fid],[? AS preset],[? AS permissions] FROM [users_groups] WHERE [is_admin] = 0 AND [is_mod] = 1") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","INSERT INTO [forums_permissions] ([gid],[fid],[preset],[permissions]) SELECT [gid],[? AS fid],[? AS preset],[? AS permissions] FROM [users_groups] WHERE [is_admin] = 0 AND [is_mod] = 1") return err } log.Print("Preparing addForumPermsToForumMembers statement.") - addForumPermsToForumMembersStmt, err = db.Prepare("") + addForumPermsToForumMembersStmt, err = db.Prepare("INSERT INTO [forums_permissions] ([gid],[fid],[preset],[permissions]) SELECT [gid],[? AS fid],[? AS preset],[? AS permissions] FROM [users_groups] WHERE [is_admin] = 0 AND [is_mod] = 0 AND [is_banned] = 0") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","INSERT INTO [forums_permissions] ([gid],[fid],[preset],[permissions]) SELECT [gid],[? AS fid],[? AS preset],[? AS permissions] FROM [users_groups] WHERE [is_admin] = 0 AND [is_mod] = 0 AND [is_banned] = 0") return err } log.Print("Preparing notifyWatchers statement.") - notifyWatchersStmt, err = db.Prepare("") + notifyWatchersStmt, err = db.Prepare("INSERT INTO [activity_stream_matches] ([watcher],[asid]) SELECT [activity_subscriptions].[user],[activity_stream].[asid] FROM [activity_stream] INNER JOIN [activity_subscriptions] ON [activity_subscriptions].[targetType] = [activity_stream].[elementType] AND [activity_subscriptions].[targetID] = [activity_stream].[elementID] AND [activity_subscriptions].[user] != [activity_stream].[actor] WHERE [asid] = ?1") if err != nil { - log.Print("Bad Query: ","") + log.Print("Bad Query: ","INSERT INTO [activity_stream_matches] ([watcher],[asid]) SELECT [activity_subscriptions].[user],[activity_stream].[asid] FROM [activity_stream] INNER JOIN [activity_subscriptions] ON [activity_subscriptions].[targetType] = [activity_stream].[elementType] AND [activity_subscriptions].[targetID] = [activity_stream].[elementID] AND [activity_subscriptions].[user] != [activity_stream].[actor] WHERE [asid] = ?1") return err } diff --git a/gen_mysql.go b/gen_mysql.go index e7c8a27f..78bff293 100644 --- a/gen_mysql.go +++ b/gen_mysql.go @@ -6,6 +6,7 @@ package main import "log" import "database/sql" +import "./query_gen/lib" // nolint var getUserStmt *sql.Stmt @@ -71,8 +72,8 @@ var addModlogEntryStmt *sql.Stmt var addAdminlogEntryStmt *sql.Stmt var addAttachmentStmt *sql.Stmt var createWordFilterStmt *sql.Stmt -var addForumPermsToGroupStmt *sql.Stmt -var replaceScheduleGroupStmt *sql.Stmt +var addForumPermsToGroupStmt *qgen.MySQLUpsertCallback +var replaceScheduleGroupStmt *qgen.MySQLUpsertCallback var addRepliesToTopicStmt *sql.Stmt var removeRepliesFromTopicStmt *sql.Stmt var addTopicsToForumStmt *sql.Stmt @@ -114,6 +115,7 @@ var verifyEmailStmt *sql.Stmt var setTempGroupStmt *sql.Stmt var updateWordFilterStmt *sql.Stmt var bumpSyncStmt *sql.Stmt +var deleteUserStmt *sql.Stmt var deleteReplyStmt *sql.Stmt var deleteProfileReplyStmt *sql.Stmt var deleteForumPermsByForumStmt *sql.Stmt @@ -230,7 +232,7 @@ func _gen_mysql() (err error) { } log.Print("Preparing getUsersOffset statement.") - getUsersOffsetStmt, err = db.Prepare("SELECT `uid`,`name`,`group`,`active`,`is_super_admin`,`avatar` FROM `users` LIMIT ?,?") + getUsersOffsetStmt, err = db.Prepare("SELECT `uid`,`name`,`group`,`active`,`is_super_admin`,`avatar` FROM `users` ORDER BY uid ASC LIMIT ?,?") if err != nil { return err } @@ -254,7 +256,7 @@ func _gen_mysql() (err error) { } log.Print("Preparing getModlogsOffset statement.") - getModlogsOffsetStmt, err = db.Prepare("SELECT `action`,`elementID`,`elementType`,`ipaddress`,`actorID`,`doneAt` FROM `moderation_logs` LIMIT ?,?") + getModlogsOffsetStmt, err = db.Prepare("SELECT `action`,`elementID`,`elementType`,`ipaddress`,`actorID`,`doneAt` FROM `moderation_logs` ORDER BY doneAt DESC LIMIT ?,?") if err != nil { return err } @@ -350,7 +352,7 @@ func _gen_mysql() (err error) { } log.Print("Preparing getTopicRepliesOffset statement.") - getTopicRepliesOffsetStmt, err = db.Prepare("SELECT `replies`.`rid`,`replies`.`content`,`replies`.`createdBy`,`replies`.`createdAt`,`replies`.`lastEdit`,`replies`.`lastEditBy`,`users`.`avatar`,`users`.`name`,`users`.`group`,`users`.`url_prefix`,`users`.`url_name`,`users`.`level`,`replies`.`ipaddress`,`replies`.`likeCount`,`replies`.`actionType` FROM `replies` LEFT JOIN `users` ON `replies`.`createdBy` = `users`.`uid` WHERE `replies`.`tid` = ? LIMIT ?,?") + getTopicRepliesOffsetStmt, err = db.Prepare("SELECT `replies`.`rid`,`replies`.`content`,`replies`.`createdBy`,`replies`.`createdAt`,`replies`.`lastEdit`,`replies`.`lastEditBy`,`users`.`avatar`,`users`.`name`,`users`.`group`,`users`.`url_prefix`,`users`.`url_name`,`users`.`level`,`replies`.`ipaddress`,`replies`.`likeCount`,`replies`.`actionType` FROM `replies` LEFT JOIN `users` ON `replies`.`createdBy` = `users`.`uid` WHERE `replies`.`tid` = ? ORDER BY replies.rid ASC LIMIT ?,?") if err != nil { return err } @@ -404,19 +406,19 @@ func _gen_mysql() (err error) { } log.Print("Preparing createReport statement.") - createReportStmt, err = db.Prepare("INSERT INTO `topics`(`title`,`content`,`parsed_content`,`createdAt`,`lastReplyAt`,`createdBy`,`data`,`parentID`,`css_class`) VALUES (?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,1,'report')") + createReportStmt, err = db.Prepare("INSERT INTO `topics`(`title`,`content`,`parsed_content`,`createdAt`,`lastReplyAt`,`createdBy`,`lastReplyBy`,`data`,`parentID`,`css_class`) VALUES (?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?,1,'report')") if err != nil { return err } log.Print("Preparing createReply statement.") - createReplyStmt, err = db.Prepare("INSERT INTO `replies`(`tid`,`content`,`parsed_content`,`createdAt`,`ipaddress`,`words`,`createdBy`) VALUES (?,?,?,UTC_TIMESTAMP(),?,?,?)") + createReplyStmt, err = db.Prepare("INSERT INTO `replies`(`tid`,`content`,`parsed_content`,`createdAt`,`lastUpdated`,`ipaddress`,`words`,`createdBy`) VALUES (?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?)") if err != nil { return err } log.Print("Preparing createActionReply statement.") - createActionReplyStmt, err = db.Prepare("INSERT INTO `replies`(`tid`,`actionType`,`ipaddress`,`createdBy`) VALUES (?,?,?,?)") + createActionReplyStmt, err = db.Prepare("INSERT INTO `replies`(`tid`,`actionType`,`ipaddress`,`createdBy`,`createdAt`,`lastUpdated`,`content`,`parsed_content`) VALUES (?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),'','')") if err != nil { return err } @@ -512,13 +514,13 @@ func _gen_mysql() (err error) { } log.Print("Preparing addForumPermsToGroup statement.") - addForumPermsToGroupStmt, err = db.Prepare("REPLACE INTO `forums_permissions`(`gid`,`fid`,`preset`,`permissions`) VALUES (?,?,?,?)") + addForumPermsToGroupStmt, err = qgen.PrepareMySQLUpsertCallback(db, "INSERT INTO `forums_permissions`(`gid`,`fid`,`preset`,`permissions`) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE `gid` = ? AND `fid` = ? AND `preset` = ? AND `permissions` = ?") if err != nil { return err } log.Print("Preparing replaceScheduleGroup statement.") - replaceScheduleGroupStmt, err = db.Prepare("REPLACE INTO `users_groups_scheduler`(`uid`,`set_group`,`issued_by`,`issued_at`,`revert_at`,`temporary`) VALUES (?,?,?,UTC_TIMESTAMP(),?,?)") + replaceScheduleGroupStmt, err = qgen.PrepareMySQLUpsertCallback(db, "INSERT INTO `users_groups_scheduler`(`uid`,`set_group`,`issued_by`,`issued_at`,`revert_at`,`temporary`) VALUES (?,?,?,UTC_TIMESTAMP(),?,?) ON DUPLICATE KEY UPDATE `uid` = ? AND `set_group` = ? AND `issued_by` = ? AND `issued_at` = UTC_TIMESTAMP() AND `revert_at` = ? AND `temporary` = ?") if err != nil { return err } @@ -769,6 +771,12 @@ func _gen_mysql() (err error) { return err } + log.Print("Preparing deleteUser statement.") + deleteUserStmt, err = db.Prepare("DELETE FROM `users` WHERE `uid` = ?") + if err != nil { + return err + } + log.Print("Preparing deleteReply statement.") deleteReplyStmt, err = db.Prepare("DELETE FROM `replies` WHERE `rid` = ?") if err != nil { diff --git a/gen_tables.go b/gen_tables.go new file mode 100644 index 00000000..e8fc1617 --- /dev/null +++ b/gen_tables.go @@ -0,0 +1,15 @@ +// Generated by Gosora's Query Generator. DO NOT EDIT. +package main + +var dbTablePrimaryKeys = map[string]string{ + "forums":"fid", + "topics":"tid", + "attachments":"attachID", + "users_replies":"rid", + "word_filters":"wfid", + "users":"uid", + "users_groups":"gid", + "users_groups_scheduler":"uid", + "replies":"rid", + "activity_stream":"asid", +} diff --git a/general_test.go b/general_test.go index 4ade42d7..9a3df87c 100644 --- a/general_test.go +++ b/general_test.go @@ -24,6 +24,26 @@ import ( //var dbTest *sql.DB var dbProd *sql.DB var gloinited bool +var installAdapter install.InstallAdapter + +func ResetTables() (err error) { + err = installAdapter.InitDatabase() + if err != nil { + return err + } + + err = installAdapter.TableDefs() + if err != nil { + return err + } + + err = installAdapter.CreateAdmin() + if err != nil { + return err + } + + return installAdapter.InitialData() +} func gloinit() (err error) { dev.DebugMode = false @@ -42,28 +62,14 @@ func gloinit() (err error) { switchToTestDB() - adap, ok := install.Lookup(dbAdapter) + var ok bool + installAdapter, ok = install.Lookup(dbAdapter) if !ok { return errors.New("We couldn't find the adapter '" + dbAdapter + "'") } - adap.SetConfig(dbConfig.Host, dbConfig.Username, dbConfig.Password, dbConfig.Dbname, dbConfig.Port) + installAdapter.SetConfig(dbConfig.Host, dbConfig.Username, dbConfig.Password, dbConfig.Dbname, dbConfig.Port) - err = adap.InitDatabase() - if err != nil { - return err - } - - err = adap.TableDefs() - if err != nil { - return err - } - - err = adap.CreateAdmin() - if err != nil { - return err - } - - err = adap.InitialData() + err = ResetTables() if err != nil { return err } @@ -110,6 +116,7 @@ func gloinit() (err error) { func init() { err := gloinit() if err != nil { + log.Print("Something bad happened") log.Fatal(err) } } diff --git a/install/install/mssql.go b/install/install/mssql.go index 58c95ed1..672983c6 100644 --- a/install/install/mssql.go +++ b/install/install/mssql.go @@ -106,6 +106,13 @@ func (ins *MssqlInstaller) TableDefs() (err error) { } table = strings.TrimSuffix(table, ext) + // ? - This is mainly here for tests, although it might allow the installer to overwrite a production database, so we might want to proceed with caution + _, err = ins.db.Exec("DROP TABLE IF EXISTS [" + table + "];") + if err != nil { + fmt.Println("Failed query:", "DROP TABLE IF EXISTS ["+table+"]") + return err + } + fmt.Println("Creating table '" + table + "'") data, err := ioutil.ReadFile("./schema/mssql/" + f.Name()) if err != nil { diff --git a/member_routes.go b/member_routes.go index ea848574..6711b3c3 100644 --- a/member_routes.go +++ b/member_routes.go @@ -758,7 +758,7 @@ func routeReportSubmit(w http.ResponseWriter, r *http.Request, user User, sitemI // TODO: Repost attachments in the reports forum, so that the mods can see them // ? - Can we do this via the TopicStore? - res, err := createReportStmt.Exec(title, content, parseMessage(content, 0, ""), user.ID, itemType+"_"+strconv.Itoa(itemID)) + res, err := createReportStmt.Exec(title, content, parseMessage(content, 0, ""), user.ID, user.ID, itemType+"_"+strconv.Itoa(itemID)) if err != nil { InternalError(err, w) return diff --git a/misc_test.go b/misc_test.go index e142fe1c..596e9e3b 100644 --- a/misc_test.go +++ b/misc_test.go @@ -1,10 +1,29 @@ package main -import "strconv" -import "testing" +import ( + "fmt" + "runtime/debug" + "strconv" + "testing" + "time" +) + +func recordMustExist(t *testing.T, err error, errmsg string) { + if err == ErrNoRows { + t.Error(errmsg) + } else if err != nil { + t.Fatal(err) + } +} + +func recordMustNotExist(t *testing.T, err error, errmsg string) { + if err == nil { + t.Error(errmsg) + } else if err != ErrNoRows { + t.Fatal(err) + } +} -// TODO: Generate a test database to work with rather than a live one -// TODO: We might need to refactor TestUserStore soon, as it's getting fairly complex func TestUserStore(t *testing.T) { if !gloinited { err := gloinit() @@ -18,14 +37,13 @@ func TestUserStore(t *testing.T) { users = NewMemoryUserStore(config.UserCacheCapacity) users.(UserCache).Flush() - userStoreTest(t) + userStoreTest(t, 2) users = NewSQLUserStore() - userStoreTest(t) + userStoreTest(t, 3) } -func userStoreTest(t *testing.T) { +func userStoreTest(t *testing.T, newUserID int) { var user *User var err error - var length int ucache, hasCache := users.(UserCache) if hasCache && ucache.Length() != 0 { @@ -33,60 +51,57 @@ func userStoreTest(t *testing.T) { } _, err = users.Get(-1) - if err == nil { - t.Error("UID #-1 shouldn't exist") - } else if err != ErrNoRows { - t.Fatal(err) - } + recordMustNotExist(t, err, "UID #-1 shouldn't exist") if hasCache && ucache.Length() != 0 { t.Error("There shouldn't be anything in the user cache") } _, err = users.Get(0) - if err == nil { - t.Error("UID #0 shouldn't exist") - } else if err != ErrNoRows { - t.Fatal(err) - } + recordMustNotExist(t, err, "UID #0 shouldn't exist") if hasCache && ucache.Length() != 0 { t.Error("There shouldn't be anything in the user cache") } user, err = users.Get(1) - if err == ErrNoRows { - t.Error("Couldn't find UID #1") - } else if err != nil { - t.Fatal(err) - } + recordMustExist(t, err, "Couldn't find UID #1") if user.ID != 1 { t.Error("user.ID does not match the requested UID. Got '" + strconv.Itoa(user.ID) + "' instead.") } + if user.Name != "Admin" { + t.Error("user.Name should be 'Admin', not '" + user.Name + "'") + } + if user.Group != 1 { + t.Error("Admin should be in group 1") + } + + user, err = users.Get(newUserID) + recordMustNotExist(t, err, fmt.Sprintf("UID #%d shouldn't exist", newUserID)) if hasCache { - length = ucache.Length() - if length != 1 { - t.Error("User cache length should be 1, not " + strconv.Itoa(length)) - } + expectIntToBeX(t, ucache.Length(), 1, "User cache length should be 1, not %d") + user, err = ucache.CacheGet(-1) + recordMustNotExist(t, err, "UID #-1 shouldn't exist, even in the cache") + user, err = ucache.CacheGet(0) + recordMustNotExist(t, err, "UID #0 shouldn't exist, even in the cache") user, err = ucache.CacheGet(1) - if err == ErrNoRows { - t.Error("Couldn't find UID #1 in the cache") - } else if err != nil { - t.Fatal(err) - } + recordMustExist(t, err, "Couldn't find UID #1 in the cache") if user.ID != 1 { t.Error("user.ID does not match the requested UID. Got '" + strconv.Itoa(user.ID) + "' instead.") } + if user.Name != "Admin" { + t.Error("user.Name should be 'Admin', not '" + user.Name + "'") + } + + user, err = ucache.CacheGet(newUserID) + recordMustNotExist(t, err, fmt.Sprintf("UID #%d shouldn't exist, even in the cache", newUserID)) ucache.Flush() - length = ucache.Length() - if length != 0 { - t.Error("User cache length should be 0, not " + strconv.Itoa(length)) - } + expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d") } // TODO: Lock onto the specific error type. Is this even possible without sacrificing the detailed information in the error message? @@ -97,10 +112,7 @@ func userStoreTest(t *testing.T) { } if hasCache { - length = ucache.Length() - if length != 0 { - t.Error("User cache length should be 0, not " + strconv.Itoa(length)) - } + expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d") } userList, _ = users.BulkGetMap([]int{0}) @@ -109,10 +121,7 @@ func userStoreTest(t *testing.T) { } if hasCache { - length = ucache.Length() - if length != 0 { - t.Error("User cache length should be 0, not " + strconv.Itoa(length)) - } + expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d") } userList, _ = users.BulkGetMap([]int{1}) @@ -133,17 +142,9 @@ func userStoreTest(t *testing.T) { } if hasCache { - length = ucache.Length() - if length != 1 { - t.Error("User cache length should be 1, not " + strconv.Itoa(length)) - } - + expectIntToBeX(t, ucache.Length(), 1, "User cache length should be 1, not %d") user, err = ucache.CacheGet(1) - if err == ErrNoRows { - t.Error("Couldn't find UID #1 in the cache") - } else if err != nil { - t.Fatal(err) - } + recordMustExist(t, err, "Couldn't find UID #1 in the cache") if user.ID != 1 { t.Error("user.ID does not match the requested UID. Got '" + strconv.Itoa(user.ID) + "' instead.") @@ -152,32 +153,138 @@ func userStoreTest(t *testing.T) { ucache.Flush() } - ok = users.Exists(-1) - if ok { - t.Error("UID #-1 shouldn't exist") - } - - ok = users.Exists(0) - if ok { - t.Error("UID #0 shouldn't exist") - } - - ok = users.Exists(1) - if !ok { - t.Error("UID #1 should exist") - } + expect(t, !users.Exists(-1), "UID #-1 shouldn't exist") + expect(t, !users.Exists(0), "UID #0 shouldn't exist") + expect(t, users.Exists(1), "UID #1 should exist") + expect(t, !users.Exists(newUserID), fmt.Sprintf("UID #%d shouldn't exist", newUserID)) if hasCache { - length = ucache.Length() - if length != 0 { - t.Error("User cache length should be 0, not " + strconv.Itoa(length)) + expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d") + } + expectIntToBeX(t, users.GlobalCount(), 1, "The number of users should be one, not %d") + + var awaitingActivation = 5 + uid, err := users.Create("Sam", "ReallyBadPassword", "sam@localhost.loc", awaitingActivation, 0) + if err != nil { + t.Error(err) + } + if uid != newUserID { + t.Errorf("The UID of the new user should be %d", newUserID) + } + if !users.Exists(newUserID) { + t.Errorf("UID #%d should exist", newUserID) + } + + user, err = users.Get(newUserID) + recordMustExist(t, err, fmt.Sprintf("Couldn't find UID #%d", newUserID)) + if user.ID != newUserID { + t.Errorf("The UID of the user record should be %d", newUserID) + } + if user.Name != "Sam" { + t.Error("The user should be named Sam") + } + expectIntToBeX(t, user.Group, 5, "Sam should be in group 5") + + if hasCache { + expectIntToBeX(t, ucache.Length(), 1, "User cache length should be 1, not %d") + user, err = ucache.CacheGet(newUserID) + recordMustExist(t, err, fmt.Sprintf("Couldn't find UID #%d in the cache", newUserID)) + if user.ID != newUserID { + t.Error("user.ID does not match the requested UID. Got '" + strconv.Itoa(user.ID) + "' instead.") } } - count := users.GlobalCount() - if count <= 0 { - t.Error("The number of users should be bigger than zero") - t.Error("count", count) + err = user.Activate() + if err != nil { + t.Error(err) + } + expectIntToBeX(t, user.Group, 5, "Sam should still be in group 5 in this copy") + + // ? - What if we change the caching mechanism so it isn't hard purged and reloaded? We'll deal with that when we come to it, but for now, this is a sign of a cache bug + if hasCache { + expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d") + _, err = ucache.CacheGet(newUserID) + recordMustNotExist(t, err, fmt.Sprintf("UID #%d shouldn't be in the cache", newUserID)) + } + + user, err = users.Get(newUserID) + recordMustExist(t, err, fmt.Sprintf("Couldn't find UID #%d", newUserID)) + if user.ID != newUserID { + t.Errorf("The UID of the user record should be %d", newUserID) + } + expectIntToBeX(t, user.Group, config.DefaultGroup, "Sam should be in group "+strconv.Itoa(config.DefaultGroup)) + + // Permanent ban + duration, _ := time.ParseDuration("0") + + // TODO: Attempt a double ban, double activation, and double unban + err = user.Ban(duration, 1) + if err != nil { + t.Error(err) + } + expectIntToBeX(t, user.Group, config.DefaultGroup, "Sam should still be in the default group in this copy") + + if hasCache { + expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d") + _, err = ucache.CacheGet(2) + recordMustNotExist(t, err, fmt.Sprintf("UID #%d shouldn't be in the cache", newUserID)) + } + + user, err = users.Get(newUserID) + recordMustExist(t, err, fmt.Sprintf("Couldn't find UID #%d", newUserID)) + if user.ID != newUserID { + t.Errorf("The UID of the user record should be %d", newUserID) + } + expectIntToBeX(t, user.Group, banGroup, "Sam should be in group "+strconv.Itoa(banGroup)) + + // TODO: Do tests against the scheduled updates table and the task system to make sure the ban exists there and gets revoked when it should + + err = user.Unban() + if err != nil { + t.Error(err) + } + expectIntToBeX(t, user.Group, banGroup, "Sam should still be in the ban group in this copy") + + if hasCache { + expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d") + _, err = ucache.CacheGet(newUserID) + recordMustNotExist(t, err, fmt.Sprintf("UID #%d shouldn't be in the cache", newUserID)) + } + + user, err = users.Get(newUserID) + recordMustExist(t, err, fmt.Sprintf("Couldn't find UID #%d", newUserID)) + if user.ID != newUserID { + t.Errorf("The UID of the user record should be %d", newUserID) + } + expectIntToBeX(t, user.Group, config.DefaultGroup, "Sam should be back in group "+strconv.Itoa(config.DefaultGroup)) + + err = user.Delete() + if err != nil { + t.Error(err) + } + expect(t, !users.Exists(newUserID), fmt.Sprintf("UID #%d should not longer exist", newUserID)) + + if hasCache { + expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d") + _, err = ucache.CacheGet(newUserID) + recordMustNotExist(t, err, fmt.Sprintf("UID #%d shouldn't be in the cache", newUserID)) + } + + // TODO: Works for now but might cause a data race with the task system + //ResetTables() +} + +func expectIntToBeX(t *testing.T, item int, expect int, errmsg string) { + if item != expect { + debug.PrintStack() + t.Fatalf(errmsg, item) + } +} + +func expect(t *testing.T, item bool, errmsg string) { + if !item { + debug.PrintStack() + t.Fatalf(errmsg) } } @@ -216,11 +323,7 @@ func topicStoreTest(t *testing.T) { } topic, err = topics.Get(1) - if err == ErrNoRows { - t.Error("Couldn't find TID #1") - } else if err != nil { - t.Fatal(err) - } + recordMustExist(t, err, "Couldn't find TID #1") if topic.ID != 1 { t.Error("topic.ID does not match the requested TID. Got '" + strconv.Itoa(topic.ID) + "' instead.") @@ -276,11 +379,7 @@ func TestForumStore(t *testing.T) { } forum, err = fstore.Get(1) - if err == ErrNoRows { - t.Error("Couldn't find FID #1") - } else if err != nil { - t.Fatal(err) - } + recordMustExist(t, err, "Couldn't find FID #1") if forum.ID != 1 { t.Error("forum.ID doesn't not match the requested FID. Got '" + strconv.Itoa(forum.ID) + "' instead.'") @@ -290,11 +389,7 @@ func TestForumStore(t *testing.T) { } forum, err = fstore.Get(2) - if err == ErrNoRows { - t.Error("Couldn't find FID #2") - } else if err != nil { - t.Fatal(err) - } + recordMustExist(t, err, "Couldn't find FID #1") _ = forum @@ -332,12 +427,9 @@ func TestGroupStore(t *testing.T) { t.Fatal(err) } + // TODO: Refactor the group store to remove GID #0 group, err = gstore.Get(0) - if err == ErrNoRows { - t.Error("Couldn't find GID #0") - } else if err != nil { - t.Fatal(err) - } + recordMustExist(t, err, "Couldn't find GID #0") if group.ID != 0 { t.Error("group.ID doesn't not match the requested GID. Got '" + strconv.Itoa(group.ID) + "' instead.") @@ -346,14 +438,8 @@ func TestGroupStore(t *testing.T) { t.Error("GID #0 is named '" + group.Name + "' and not 'Unknown'") } - // ? - What if they delete this group? x.x - // ? - Maybe, pick a random group ID? That would take an extra query, and I'm not sure if I want to be rewriting custom test queries. Possibly, a Random() method on the GroupStore? Seems useless for normal use, it might have some merit for the TopicStore though group, err = gstore.Get(1) - if err == ErrNoRows { - t.Error("Couldn't find GID #1") - } else if err != nil { - t.Fatal(err) - } + recordMustExist(t, err, "Couldn't find GID #1") if group.ID != 1 { t.Error("group.ID doesn't not match the requested GID. Got '" + strconv.Itoa(group.ID) + "' instead.'") diff --git a/mssql.go b/mssql.go index 637a3ea0..2ce0cfd8 100644 --- a/mssql.go +++ b/mssql.go @@ -11,6 +11,7 @@ package main //import "time" import ( "database/sql" + "log" "net/url" "./query_gen/lib" @@ -78,7 +79,62 @@ func initMSSQL() (err error) { return err } - // TODO: Add the custom queries + setter, ok := qgen.Builder.GetAdapter().(qgen.SetPrimaryKeys) + if ok { + setter.SetPrimaryKeys(dbTablePrimaryKeys) + } - return nil + // TODO: Is there a less noisy way of doing this for tests? + log.Print("Preparing get_activity_feed_by_watcher statement.") + getActivityFeedByWatcherStmt, err = db.Prepare("SELECT activity_stream_matches.asid, activity_stream.actor, activity_stream.targetUser, activity_stream.event, activity_stream.elementType, activity_stream.elementID FROM [activity_stream_matches] INNER JOIN [activity_stream] ON activity_stream_matches.asid = activity_stream.asid AND activity_stream_matches.watcher != activity_stream.actor WHERE [watcher] = ? ORDER BY activity_stream.asid ASC OFFSET 0 ROWS FETCH NEXT 8 ROWS ONLY") + if err != nil { + return err + } + + log.Print("Preparing get_activity_count_by_watcher statement.") + getActivityCountByWatcherStmt, err = db.Prepare("SELECT count(*) FROM [activity_stream_matches] INNER JOIN [activity_stream] ON activity_stream_matches.asid = activity_stream.asid AND activity_stream_matches.watcher != activity_stream.actor WHERE [watcher] = ?") + if err != nil { + return err + } + + log.Print("Preparing todays_post_count statement.") + todaysPostCountStmt, err = db.Prepare("select count(*) from replies where createdAt >= DATEADD(DAY, -1, GETUTCDATE())") + if err != nil { + return err + } + + log.Print("Preparing todays_topic_count statement.") + todaysTopicCountStmt, err = db.Prepare("select count(*) from topics where createdAt >= DATEADD(DAY, -1, GETUTCDATE())") + if err != nil { + return err + } + + log.Print("Preparing todays_report_count statement.") + todaysReportCountStmt, err = db.Prepare("select count(*) from topics where createdAt >= DATEADD(DAY, -1, GETUTCDATE()) and parentID = 1") + if err != nil { + return err + } + + log.Print("Preparing todays_newuser_count statement.") + todaysNewUserCountStmt, err = db.Prepare("select count(*) from users where createdAt >= DATEADD(DAY, -1, GETUTCDATE())") + if err != nil { + return err + } + + // ? - Why is this a custom query? Are we planning a union or something? + log.Print("Preparing find_users_by_ip_users statement.") + findUsersByIPUsersStmt, err = db.Prepare("select uid from users where last_ip = ?") + if err != nil { + return err + } + + log.Print("Preparing find_users_by_ip_topics statement.") + findUsersByIPTopicsStmt, err = db.Prepare("select uid from users where uid in(select createdBy from topics where ipaddress = ?)") + if err != nil { + return err + } + + log.Print("Preparing find_users_by_ip_replies statement.") + findUsersByIPRepliesStmt, err = db.Prepare("select uid from users where uid in(select createdBy from replies where ipaddress = ?)") + return err } diff --git a/plugin_test.go b/plugin_test.go index 910b5a91..8a0fe2b7 100644 --- a/plugin_test.go +++ b/plugin_test.go @@ -180,7 +180,7 @@ func TestBBCodeRender(t *testing.T) { t.Error("Expected a number between 0 and 170141183460469231731687303715884105727") } - t.Log("Testing bbcode_regex_parse") + /*t.Log("Testing bbcode_regex_parse") for _, item := range msgList { t.Log("Testing string '" + item.Msg + "'") res = bbcodeRegexParse(item.Msg) @@ -188,7 +188,7 @@ func TestBBCodeRender(t *testing.T) { t.Error("Bad output:", "'"+res+"'") t.Error("Expected:", item.Expects) } - } + }*/ } func TestMarkdownRender(t *testing.T) { diff --git a/public/global.js b/public/global.js index 0dbcd886..3a058ad6 100644 --- a/public/global.js +++ b/public/global.js @@ -210,22 +210,22 @@ $(document).ready(function(){ $(".hide_on_edit").show(); $(".show_on_edit").hide(); - var topic_name_input = $('.topic_name_input').val(); - var topic_status_input = $('.topic_status_input').val(); - var topic_content_input = $('.topic_content_input').val(); - var form_action = this.form.getAttribute("action"); - //console.log("New Topic Name: " + topic_name_input); - //console.log("New Topic Status: " + topic_status_input); - //console.log("New Topic Content: " + topic_content_input); - //console.log("Form Action: " + form_action); + let topicNameInput = $('.topic_name_input').val(); + let topicStatusInput = $('.topic_status_input').val(); + let topicContentInput = $('.topic_content_input').val(); + let formAction = this.form.getAttribute("action"); + //console.log("New Topic Name: " + topicNameInput); + //console.log("New Topic Status: " + topicStatusInput); + //console.log("New Topic Content: " + topicContentInput); + //console.log("Form Action: " + formAction); $.ajax({ - url: form_action, + url: formAction, type: "POST", dataType: "json", data: { - topic_name: topic_name_input, - topic_status: topic_status_input, - topic_content: topic_content_input, + topic_name: topicNameInput, + topic_status: topicStatusInput, + topic_content: topicContentInput, topic_js: 1 } }); @@ -234,28 +234,27 @@ $(document).ready(function(){ $(".delete_item").click(function(event) { post_link(event); - var block = $(this).closest('.deletable_block'); - block.remove(); + $(this).closest('.deletable_block').remove(); }); $(".edit_item").click(function(event) { event.preventDefault(); - var block_parent = $(this).closest('.editable_parent'); - var block = block_parent.find('.editable_block').eq(0); + let blockParent = $(this).closest('.editable_parent'); + let block = blockParent.find('.editable_block').eq(0); block.html("
"); $(".submit_edit").click(function(event) { event.preventDefault(); - var block_parent = $(this).closest('.editable_parent'); - var block = block_parent.find('.editable_block').eq(0); - var newContent = block.find('textarea').eq(0).val(); + let blockParent = $(this).closest('.editable_parent'); + let block = blockParent.find('.editable_block').eq(0); + let newContent = block.find('textarea').eq(0).val(); block.html(newContent); - var form_action = $(this).closest('a').attr("href"); + var formAction = $(this).closest('a').attr("href"); //console.log("Form Action: " + form_action); - $.ajax({ url: form_action, type: "POST", dataType: "json", data: { isJs: "1", edit_item: newContent } + $.ajax({ url: formAction, type: "POST", dataType: "json", data: { isJs: "1", edit_item: newContent } }); }); }); @@ -263,22 +262,22 @@ $(document).ready(function(){ $(".edit_field").click(function(event) { event.preventDefault(); - var block_parent = $(this).closest('.editable_parent'); - var block = block_parent.find('.editable_block').eq(0); + let blockParent = $(this).closest('.editable_parent'); + let block = blockParent.find('.editable_block').eq(0); block.html(""); $(".submit_edit").click(function(event) { event.preventDefault(); - var block_parent = $(this).closest('.editable_parent'); - var block = block_parent.find('.editable_block').eq(0); - var newContent = block.find('input').eq(0).val(); + let blockParent = $(this).closest('.editable_parent'); + let block = blockParent.find('.editable_block').eq(0); + let newContent = block.find('input').eq(0).val(); block.html(newContent); - var form_action = $(this).closest('a').attr("href"); - //console.log("Form Action: " + form_action); + let formAction = $(this).closest('a').attr("href"); + //console.log("Form Action: " + formAction); $.ajax({ - url: form_action + "?session=" + session, + url: formAction + "?session=" + session, type: "POST", dataType: "json", data: {isJs: "1",edit_item: newContent} diff --git a/query_gen/lib/builder.go b/query_gen/lib/builder.go index 1a54a0d7..62788adb 100644 --- a/query_gen/lib/builder.go +++ b/query_gen/lib/builder.go @@ -7,13 +7,12 @@ import "database/sql" var Builder *builder func init() { - Builder = &builder{conn:nil} + Builder = &builder{conn: nil} } // A set of wrappers around the generator methods, so that we can use this inline in Gosora -type builder struct -{ - conn *sql.DB +type builder struct { + conn *sql.DB adapter DB_Adapter } @@ -30,6 +29,10 @@ func (build *builder) SetAdapter(name string) error { return nil } +func (build *builder) GetAdapter() DB_Adapter { + return build.adapter +} + func (build *builder) SimpleSelect(table string, columns string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) { res, err := build.adapter.SimpleSelect("_builder", table, columns, where, orderby, limit) if err != nil { diff --git a/query_gen/lib/install.go b/query_gen/lib/install.go index 2c5d638a..1bc39c91 100644 --- a/query_gen/lib/install.go +++ b/query_gen/lib/install.go @@ -18,6 +18,7 @@ type DB_Install_Instruction struct { type installer struct { adapter DB_Adapter instructions []DB_Install_Instruction + plugins []QueryPlugin } func (install *installer) SetAdapter(name string) error { @@ -35,20 +36,48 @@ func (install *installer) SetAdapterInstance(adapter DB_Adapter) { install.instructions = []DB_Install_Instruction{} } +func (install *installer) RegisterPlugin(plugin QueryPlugin) { + install.plugins = append(install.plugins, plugin) +} + func (install *installer) CreateTable(table string, charset string, collation string, columns []DB_Table_Column, keys []DB_Table_Key) error { + for _, plugin := range install.plugins { + err := plugin.Hook("CreateTableStart", table, charset, collation, columns, keys) + if err != nil { + return err + } + } res, err := install.adapter.CreateTable("_installer", table, charset, collation, columns, keys) if err != nil { return err } + for _, plugin := range install.plugins { + err := plugin.Hook("CreateTableAfter", table, charset, collation, columns, keys, res) + if err != nil { + return err + } + } install.instructions = append(install.instructions, DB_Install_Instruction{table, res, "create-table"}) return nil } func (install *installer) SimpleInsert(table string, columns string, fields string) error { + for _, plugin := range install.plugins { + err := plugin.Hook("SimpleInsertStart", table, columns, fields) + if err != nil { + return err + } + } res, err := install.adapter.SimpleInsert("_installer", table, columns, fields) if err != nil { return err } + for _, plugin := range install.plugins { + err := plugin.Hook("SimpleInsertAfter", table, columns, fields, res) + if err != nil { + return err + } + } install.instructions = append(install.instructions, DB_Install_Instruction{table, res, "insert"}) return nil } @@ -66,5 +95,18 @@ func (install *installer) Write() error { inserts += instr.Contents + ";\n" } } - return writeFile("./schema/"+install.adapter.GetName()+"/inserts.sql", inserts) + + err := writeFile("./schema/"+install.adapter.GetName()+"/inserts.sql", inserts) + if err != nil { + return err + } + + for _, plugin := range install.plugins { + err := plugin.Write() + if err != nil { + return err + } + } + + return nil } diff --git a/query_gen/lib/mssql.go b/query_gen/lib/mssql.go new file mode 100644 index 00000000..077c09e2 --- /dev/null +++ b/query_gen/lib/mssql.go @@ -0,0 +1,1130 @@ +/* WIP Under Really Heavy Construction */ +package qgen + +import ( + "errors" + "log" + "strconv" + "strings" +) + +func init() { + DB_Registry = append(DB_Registry, + &Mssql_Adapter{Name: "mssql", Buffer: make(map[string]DB_Stmt)}, + ) +} + +type Mssql_Adapter struct { + Name string // ? - Do we really need this? Can't we hard-code this? + Buffer map[string]DB_Stmt + BufferOrder []string // Map iteration order is random, so we need this to track the order, so we don't get huge diffs every commit + keys map[string]string +} + +// GetName gives you the name of the database adapter. In this case, it's Mssql +func (adapter *Mssql_Adapter) GetName() string { + return adapter.Name +} + +func (adapter *Mssql_Adapter) GetStmt(name string) DB_Stmt { + return adapter.Buffer[name] +} + +func (adapter *Mssql_Adapter) GetStmts() map[string]DB_Stmt { + return adapter.Buffer +} + +// TODO: Convert any remaining stringy types to nvarchar +// We may need to change the CreateTable API to better suit Mssql and the other database drivers which are coming up +func (adapter *Mssql_Adapter) CreateTable(name string, table string, charset string, collation string, columns []DB_Table_Column, keys []DB_Table_Key) (string, error) { + if name == "" { + return "", errors.New("You need a name for this statement") + } + if table == "" { + return "", errors.New("You need a name for this table") + } + if len(columns) == 0 { + return "", errors.New("You can't have a table with no columns") + } + + var querystr = "CREATE TABLE [" + table + "] (" + for _, column := range columns { + var max bool + var createdAt bool + switch column.Type { + case "createdAt": + column.Type = "datetime" + createdAt = true + case "varchar": + column.Type = "nvarchar" + case "text": + column.Type = "nvarchar" + max = true + case "boolean": + column.Type = "bit" + } + + var size string + if column.Size > 0 { + size = " (" + strconv.Itoa(column.Size) + ")" + } + if max { + size = " (MAX)" + } + + var end string + if column.Default != "" { + end = " DEFAULT " + if createdAt { + end += "GETUTCDATE()" // TODO: Use GETUTCDATE() in updates instead of the neutral format + } else if adapter.stringyType(column.Type) && column.Default != "''" { + end += "'" + column.Default + "'" + } else { + end += column.Default + } + } + + if !column.Null { + end += " not null" + } + + // ! Not exactly the meaning of auto increment... + if column.Auto_Increment { + end += " IDENTITY" + } + + querystr += "\n\t[" + column.Name + "] " + column.Type + size + end + "," + } + + if len(keys) > 0 { + for _, key := range keys { + querystr += "\n\t" + key.Type + if key.Type != "unique" { + querystr += " key" + } + querystr += "(" + for _, column := range strings.Split(key.Columns, ",") { + querystr += "[" + column + "]," + } + querystr = querystr[0:len(querystr)-1] + ")," + } + } + + querystr = querystr[0:len(querystr)-1] + "\n);" + adapter.pushStatement(name, "create-table", querystr) + return querystr, nil +} + +func (adapter *Mssql_Adapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) { + if name == "" { + return "", errors.New("You need a name for this statement") + } + if table == "" { + return "", errors.New("You need a name for this table") + } + if len(columns) == 0 { + return "", errors.New("No columns found for SimpleInsert") + } + if len(fields) == 0 { + return "", errors.New("No input data found for SimpleInsert") + } + + var querystr = "INSERT INTO [" + table + "] (" + + // Escape the column names, just in case we've used a reserved keyword + for _, column := range processColumns(columns) { + if column.Type == "function" { + querystr += column.Left + "," + } else { + querystr += "[" + column.Left + "]," + } + } + // Remove the trailing comma + querystr = querystr[0 : len(querystr)-1] + + querystr += ") VALUES (" + for _, field := range processFields(fields) { + field.Name = strings.Replace(field.Name, "UTC_TIMESTAMP()", "GETUTCDATE()", -1) + //log.Print("field.Name ", field.Name) + nameLen := len(field.Name) + if field.Name[0] == '"' && field.Name[nameLen-1] == '"' && nameLen >= 3 { + field.Name = "'" + field.Name[1:nameLen-1] + "'" + } + if field.Name[0] == '\'' && field.Name[nameLen-1] == '\'' && nameLen >= 3 { + field.Name = "'" + strings.Replace(field.Name[1:nameLen-1], "'", "''", -1) + "'" + } + querystr += field.Name + "," + } + querystr = querystr[0 : len(querystr)-1] + + adapter.pushStatement(name, "insert", querystr+")") + return querystr + ")", nil +} + +// ! DEPRECATED +func (adapter *Mssql_Adapter) SimpleReplace(name string, table string, columns string, fields string) (string, error) { + log.Print("In SimpleReplace") + key, ok := adapter.keys[table] + if !ok { + return "", errors.New("Unable to elide key from table '" + table + "', please use SimpleUpsert (coming soon!) instead") + } + log.Print("After the key check") + + // Escape the column names, just in case we've used a reserved keyword + var keyPosition int + for _, column := range processColumns(columns) { + if column.Left == key { + continue + } + keyPosition++ + } + + var keyValue string + for fieldID, field := range processFields(fields) { + field.Name = strings.Replace(field.Name, "UTC_TIMESTAMP()", "GETUTCDATE()", -1) + nameLen := len(field.Name) + if field.Name[0] == '"' && field.Name[nameLen-1] == '"' && nameLen >= 3 { + field.Name = "'" + field.Name[1:nameLen-1] + "'" + } + if field.Name[0] == '\'' && field.Name[nameLen-1] == '\'' && nameLen >= 3 { + field.Name = "'" + strings.Replace(field.Name[1:nameLen-1], "'", "''", -1) + "'" + } + if keyPosition == fieldID { + keyValue = field.Name + continue + } + } + return adapter.SimpleUpsert(name, table, columns, fields, "key = "+keyValue) +} + +func (adapter *Mssql_Adapter) SimpleUpsert(name string, table string, columns string, fields string, where string) (string, error) { + if name == "" { + return "", errors.New("You need a name for this statement") + } + if table == "" { + return "", errors.New("You need a name for this table") + } + if len(columns) == 0 { + return "", errors.New("No columns found for SimpleInsert") + } + if len(fields) == 0 { + return "", errors.New("No input data found for SimpleInsert") + } + + var fieldCount int + var fieldOutput string + var querystr = "MERGE [" + table + "] WITH(HOLDLOCK) as t1 USING (VALUES(" + + var parsedFields = processFields(fields) + for _, field := range parsedFields { + fieldCount++ + field.Name = strings.Replace(field.Name, "UTC_TIMESTAMP()", "GETUTCDATE()", -1) + //log.Print("field.Name ", field.Name) + nameLen := len(field.Name) + if field.Name[0] == '"' && field.Name[nameLen-1] == '"' && nameLen >= 3 { + field.Name = "'" + field.Name[1:nameLen-1] + "'" + } + if field.Name[0] == '\'' && field.Name[nameLen-1] == '\'' && nameLen >= 3 { + field.Name = "'" + strings.Replace(field.Name[1:nameLen-1], "'", "''", -1) + "'" + } + fieldOutput += field.Name + "," + } + fieldOutput = fieldOutput[0 : len(fieldOutput)-1] + querystr += fieldOutput + ")) AS updates (" + + // nolint The linter wants this to be less readable + for fieldID, _ := range parsedFields { + querystr += "f" + strconv.Itoa(fieldID) + "," + } + querystr = querystr[0 : len(querystr)-1] + querystr += ") ON " + + //querystr += "t1.[" + key + "] = " + // Add support for BETWEEN x.x + for _, loc := range processWhere(where) { + for _, token := range loc.Expr { + switch token.Type { + case "substitute": + querystr += " ?" + case "function", "operator", "number": + // TODO: Split the function case off to speed things up + if strings.ToUpper(token.Contents) == "UTC_TIMESTAMP()" { + token.Contents = "GETUTCDATE()" + } + querystr += " " + token.Contents + case "column": + querystr += " [" + token.Contents + "]" + case "string": + querystr += " '" + token.Contents + "'" + default: + panic("This token doesn't exist o_o") + } + } + } + + var matched = " WHEN MATCHED THEN UPDATE SET " + var notMatched = "WHEN NOT MATCHED THEN INSERT(" + var fieldList string + + // Escape the column names, just in case we've used a reserved keyword + for columnID, column := range processColumns(columns) { + fieldList += "f" + strconv.Itoa(columnID) + "," + if column.Type == "function" { + matched += column.Left + " = f" + strconv.Itoa(columnID) + "," + notMatched += column.Left + "," + } else { + matched += "[" + column.Left + "] = f" + strconv.Itoa(columnID) + "," + notMatched += "[" + column.Left + "]," + } + } + + matched = matched[0 : len(matched)-1] + notMatched = notMatched[0 : len(notMatched)-1] + fieldList = fieldList[0 : len(fieldList)-1] + + notMatched += ") VALUES (" + fieldList + ");" + querystr += matched + " " + notMatched + + // TODO: Run this on debug mode? + if name[0] == '_' { + log.Print(name+" query: ", querystr) + } + adapter.pushStatement(name, "upsert", querystr) + return querystr, nil +} + +func (adapter *Mssql_Adapter) SimpleUpdate(name string, table string, set string, where string) (string, error) { + if name == "" { + return "", errors.New("You need a name for this statement") + } + if table == "" { + return "", errors.New("You need a name for this table") + } + if set == "" { + return "", errors.New("You need to set data in this update statement") + } + + var querystr = "UPDATE [" + table + "] SET " + for _, item := range processSet(set) { + querystr += "[" + item.Column + "] =" + for _, token := range item.Expr { + switch token.Type { + case "substitute": + querystr += " ?" + case "function", "operator", "number": + // TODO: Split the function case off to speed things up + if strings.ToUpper(token.Contents) == "UTC_TIMESTAMP()" { + token.Contents = "GETUTCDATE()" + } + querystr += " " + token.Contents + case "column": + querystr += " [" + token.Contents + "]" + case "string": + querystr += " '" + token.Contents + "'" + default: + panic("This token doesn't exist o_o") + } + } + querystr += "," + } + // Remove the trailing comma + querystr = querystr[0 : len(querystr)-1] + + // Add support for BETWEEN x.x + if len(where) != 0 { + querystr += " WHERE" + for _, loc := range processWhere(where) { + for _, token := range loc.Expr { + switch token.Type { + case "function", "operator", "number", "substitute": + // TODO: Split the function case off to speed things up + if strings.ToUpper(token.Contents) == "UTC_TIMESTAMP()" { + token.Contents = "GETUTCDATE()" + } + querystr += " " + token.Contents + case "column": + querystr += " [" + token.Contents + "]" + case "string": + querystr += " '" + token.Contents + "'" + default: + panic("This token doesn't exist o_o") + } + } + querystr += " AND" + } + querystr = querystr[0 : len(querystr)-4] + } + + adapter.pushStatement(name, "update", querystr) + return querystr, nil +} + +func (adapter *Mssql_Adapter) SimpleDelete(name string, table string, where string) (string, error) { + if name == "" { + return "", errors.New("You need a name for this statement") + } + if table == "" { + return "", errors.New("You need a name for this table") + } + if where == "" { + return "", errors.New("You need to specify what data you want to delete") + } + + var querystr = "DELETE FROM [" + table + "] WHERE" + + // Add support for BETWEEN x.x + for _, loc := range processWhere(where) { + for _, token := range loc.Expr { + switch token.Type { + case "substitute": + querystr += " ?" + case "function", "operator", "number": + // TODO: Split the function case off to speed things up + if strings.ToUpper(token.Contents) == "UTC_TIMESTAMP()" { + token.Contents = "GETUTCDATE()" + } + querystr += " " + token.Contents + case "column": + querystr += " [" + token.Contents + "]" + case "string": + querystr += " '" + token.Contents + "'" + default: + panic("This token doesn't exist o_o") + } + } + querystr += " AND" + } + + querystr = strings.TrimSpace(querystr[0 : len(querystr)-4]) + adapter.pushStatement(name, "delete", querystr) + return querystr, nil +} + +// We don't want to accidentally wipe tables, so we'll have a seperate method for purging tables instead +func (adapter *Mssql_Adapter) Purge(name string, table string) (string, error) { + if name == "" { + return "", errors.New("You need a name for this statement") + } + if table == "" { + return "", errors.New("You need a name for this table") + } + adapter.pushStatement(name, "purge", "DELETE FROM ["+table+"]") + return "DELETE FROM [" + table + "]", nil +} + +func (adapter *Mssql_Adapter) SimpleSelect(name string, table string, columns string, where string, orderby string, limit string) (string, error) { + if name == "" { + return "", errors.New("You need a name for this statement") + } + if table == "" { + return "", errors.New("You need a name for this table") + } + if len(columns) == 0 { + return "", errors.New("No columns found for SimpleSelect") + } + // TODO: Add this to the MySQL adapter in order to make this problem more discoverable? + if len(orderby) == 0 && limit != "" { + return "", errors.New("Orderby needs to be set to use limit on Mssql") + } + + var substituteCount = 0 + var querystr = "" + + // Escape the column names, just in case we've used a reserved keyword + var colslice = strings.Split(strings.TrimSpace(columns), ",") + for _, column := range colslice { + querystr += "[" + strings.TrimSpace(column) + "]," + } + // Remove the trailing comma + querystr = querystr[0 : len(querystr)-1] + + querystr += " FROM [" + table + "]" + + // Add support for BETWEEN x.x + if len(where) != 0 { + querystr += " WHERE" + for _, loc := range processWhere(where) { + for _, token := range loc.Expr { + switch token.Type { + case "substitute": + substituteCount++ + querystr += " ?" + strconv.Itoa(substituteCount) + case "function", "operator", "number": + // TODO: Split the function case off to speed things up + if strings.ToUpper(token.Contents) == "UTC_TIMESTAMP()" { + token.Contents = "GETUTCDATE()" + } + querystr += " " + token.Contents + case "column": + querystr += " [" + token.Contents + "]" + case "string": + querystr += " '" + token.Contents + "'" + default: + panic("This token doesn't exist o_o") + } + } + querystr += " AND" + } + querystr = querystr[0 : len(querystr)-4] + } + + // TODO: MSSQL requires ORDER BY for LIMIT + if len(orderby) != 0 { + querystr += " ORDER BY " + for _, column := range processOrderby(orderby) { + // TODO: We might want to escape this column + querystr += column.Column + " " + strings.ToUpper(column.Order) + "," + } + querystr = querystr[0 : len(querystr)-1] + } + + if limit != "" { + limiter := processLimit(limit) + log.Printf("limiter: %+v\n", limiter) + if limiter.Offset != "" { + if limiter.Offset == "?" { + substituteCount++ + querystr += " OFFSET ?" + strconv.Itoa(substituteCount) + " ROWS" + } else { + querystr += " OFFSET " + limiter.Offset + " ROWS" + } + } + + /*if limiter.MaxCount != "" { + if limiter.MaxCount == "?" { + substituteCount++ + limiter.MaxCount = "?" + strconv.Itoa(substituteCount) + } + querystr = "TOP " + limiter.MaxCount + " " + querystr + }*/ + + // ! Does this work without an offset? + if limiter.MaxCount != "" { + if limiter.MaxCount == "?" { + substituteCount++ + limiter.MaxCount = "?" + strconv.Itoa(substituteCount) + } + querystr += " FETCH NEXT " + limiter.MaxCount + " ROWS ONLY " + } + } + + querystr = strings.TrimSpace("SELECT " + querystr) + // TODO: Run this on debug mode? + if name[0] == '_' && limit == "" { + log.Print(name+" query: ", querystr) + } + adapter.pushStatement(name, "select", querystr) + return querystr, nil +} + +func (adapter *Mssql_Adapter) SimpleLeftJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) { + if name == "" { + return "", errors.New("You need a name for this statement") + } + if table1 == "" { + return "", errors.New("You need a name for the left table") + } + if table2 == "" { + return "", errors.New("You need a name for the right table") + } + if len(columns) == 0 { + return "", errors.New("No columns found for SimpleLeftJoin") + } + if len(joiners) == 0 { + return "", errors.New("No joiners found for SimpleLeftJoin") + } + // TODO: Add this to the MySQL adapter in order to make this problem more discoverable? + if len(orderby) == 0 && limit != "" { + return "", errors.New("Orderby needs to be set to use limit on Mssql") + } + var substituteCount = 0 + var querystr = "" + + for _, column := range processColumns(columns) { + var source, alias string + + // Escape the column names, just in case we've used a reserved keyword + if column.Table != "" { + source = "[" + column.Table + "].[" + column.Left + "]" + } else if column.Type == "function" { + source = column.Left + } else { + source = "[" + column.Left + "]" + } + + if column.Alias != "" { + alias = " AS '" + column.Alias + "'" + } + querystr += source + alias + "," + } + // Remove the trailing comma + querystr = querystr[0 : len(querystr)-1] + + querystr += " FROM [" + table1 + "] LEFT JOIN [" + table2 + "] ON " + for _, joiner := range processJoiner(joiners) { + querystr += "[" + joiner.LeftTable + "].[" + joiner.LeftColumn + "] " + joiner.Operator + " [" + joiner.RightTable + "].[" + joiner.RightColumn + "] AND " + } + // Remove the trailing AND + querystr = querystr[0 : len(querystr)-4] + + // Add support for BETWEEN x.x + if len(where) != 0 { + querystr += " WHERE" + for _, loc := range processWhere(where) { + for _, token := range loc.Expr { + switch token.Type { + case "substitute": + substituteCount++ + querystr += " ?" + strconv.Itoa(substituteCount) + case "function", "operator", "number": + // TODO: Split the function case off to speed things up + if strings.ToUpper(token.Contents) == "UTC_TIMESTAMP()" { + token.Contents = "GETUTCDATE()" + } + querystr += " " + token.Contents + case "column": + halves := strings.Split(token.Contents, ".") + if len(halves) == 2 { + querystr += " [" + halves[0] + "].[" + halves[1] + "]" + } else { + querystr += " [" + token.Contents + "]" + } + case "string": + querystr += " '" + token.Contents + "'" + default: + panic("This token doesn't exist o_o") + } + } + querystr += " AND" + } + querystr = querystr[0 : len(querystr)-4] + } + + // TODO: MSSQL requires ORDER BY for LIMIT + if len(orderby) != 0 { + querystr += " ORDER BY " + for _, column := range processOrderby(orderby) { + log.Print("column: ", column) + // TODO: We might want to escape this column + querystr += column.Column + " " + strings.ToUpper(column.Order) + "," + } + querystr = querystr[0 : len(querystr)-1] + } else if limit != "" { + key, ok := adapter.keys[table1] + if ok { + querystr += " ORDER BY [" + table1 + "].[" + key + "]" + } + } + + if limit != "" { + limiter := processLimit(limit) + if limiter.Offset != "" { + if limiter.Offset == "?" { + substituteCount++ + querystr += " OFFSET ?" + strconv.Itoa(substituteCount) + " ROWS" + } else { + querystr += " OFFSET " + limiter.Offset + " ROWS" + } + } + + // ! Does this work without an offset? + if limiter.MaxCount != "" { + if limiter.MaxCount == "?" { + substituteCount++ + limiter.MaxCount = "?" + strconv.Itoa(substituteCount) + } + querystr += " FETCH NEXT " + limiter.MaxCount + " ROWS ONLY " + } + } + + querystr = strings.TrimSpace("SELECT " + querystr) + // TODO: Run this on debug mode? + if name[0] == '_' && limit == "" { + log.Print(name+" query: ", querystr) + } + adapter.pushStatement(name, "select", querystr) + return querystr, nil +} + +func (adapter *Mssql_Adapter) SimpleInnerJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) { + if name == "" { + return "", errors.New("You need a name for this statement") + } + if table1 == "" { + return "", errors.New("You need a name for the left table") + } + if table2 == "" { + return "", errors.New("You need a name for the right table") + } + if len(columns) == 0 { + return "", errors.New("No columns found for SimpleInnerJoin") + } + if len(joiners) == 0 { + return "", errors.New("No joiners found for SimpleInnerJoin") + } + // TODO: Add this to the MySQL adapter in order to make this problem more discoverable? + if len(orderby) == 0 && limit != "" { + return "", errors.New("Orderby needs to be set to use limit on Mssql") + } + + var substituteCount = 0 + var querystr = "" + + for _, column := range processColumns(columns) { + var source, alias string + + // Escape the column names, just in case we've used a reserved keyword + if column.Table != "" { + source = "[" + column.Table + "].[" + column.Left + "]" + } else if column.Type == "function" { + source = column.Left + } else { + source = "[" + column.Left + "]" + } + + if column.Alias != "" { + alias = " AS '" + column.Alias + "'" + } + querystr += source + alias + "," + } + // Remove the trailing comma + querystr = querystr[0 : len(querystr)-1] + + querystr += " FROM [" + table1 + "] INNER JOIN [" + table2 + "] ON " + for _, joiner := range processJoiner(joiners) { + querystr += "[" + joiner.LeftTable + "].[" + joiner.LeftColumn + "] " + joiner.Operator + " [" + joiner.RightTable + "].[" + joiner.RightColumn + "] AND " + } + // Remove the trailing AND + querystr = querystr[0 : len(querystr)-4] + + // Add support for BETWEEN x.x + if len(where) != 0 { + querystr += " WHERE" + for _, loc := range processWhere(where) { + for _, token := range loc.Expr { + switch token.Type { + case "substitute": + substituteCount++ + querystr += " ?" + strconv.Itoa(substituteCount) + case "function", "operator", "number": + // TODO: Split the function case off to speed things up + if strings.ToUpper(token.Contents) == "UTC_TIMESTAMP()" { + token.Contents = "GETUTCDATE()" + } + querystr += " " + token.Contents + case "column": + halves := strings.Split(token.Contents, ".") + if len(halves) == 2 { + querystr += " [" + halves[0] + "].[" + halves[1] + "]" + } else { + querystr += " [" + token.Contents + "]" + } + case "string": + querystr += " '" + token.Contents + "'" + default: + panic("This token doesn't exist o_o") + } + } + querystr += " AND" + } + querystr = querystr[0 : len(querystr)-4] + } + + // TODO: MSSQL requires ORDER BY for LIMIT + if len(orderby) != 0 { + querystr += " ORDER BY " + for _, column := range processOrderby(orderby) { + log.Print("column: ", column) + // TODO: We might want to escape this column + querystr += column.Column + " " + strings.ToUpper(column.Order) + "," + } + querystr = querystr[0 : len(querystr)-1] + } else if limit != "" { + key, ok := adapter.keys[table1] + if ok { + log.Print("key: ", key) + querystr += " ORDER BY [" + table1 + "].[" + key + "]" + } + } + + if limit != "" { + limiter := processLimit(limit) + if limiter.Offset != "" { + if limiter.Offset == "?" { + substituteCount++ + querystr += " OFFSET ?" + strconv.Itoa(substituteCount) + " ROWS" + } else { + querystr += " OFFSET " + limiter.Offset + " ROWS" + } + } + + // ! Does this work without an offset? + if limiter.MaxCount != "" { + if limiter.MaxCount == "?" { + substituteCount++ + limiter.MaxCount = "?" + strconv.Itoa(substituteCount) + } + querystr += " FETCH NEXT " + limiter.MaxCount + " ROWS ONLY " + } + } + + querystr = strings.TrimSpace("SELECT " + querystr) + // TODO: Run this on debug mode? + if name[0] == '_' && limit == "" { + log.Print(name+" query: ", querystr) + } + adapter.pushStatement(name, "select", querystr) + return querystr, nil +} + +func (adapter *Mssql_Adapter) SimpleInsertSelect(name string, ins DB_Insert, sel DB_Select) (string, error) { + // TODO: More errors. + // TODO: Add this to the MySQL adapter in order to make this problem more discoverable? + if len(sel.Orderby) == 0 && sel.Limit != "" { + return "", errors.New("Orderby needs to be set to use limit on Mssql") + } + + /* Insert */ + var querystr = "INSERT INTO [" + ins.Table + "] (" + + // Escape the column names, just in case we've used a reserved keyword + for _, column := range processColumns(ins.Columns) { + if column.Type == "function" { + querystr += column.Left + "," + } else { + querystr += "[" + column.Left + "]," + } + } + querystr = querystr[0:len(querystr)-1] + ") SELECT " + + /* Select */ + var substituteCount = 0 + + // Escape the column names, just in case we've used a reserved keyword + var colslice = strings.Split(strings.TrimSpace(sel.Columns), ",") + for _, column := range colslice { + querystr += "[" + strings.TrimSpace(column) + "]," + } + // Remove the trailing comma + querystr = querystr[0 : len(querystr)-1] + querystr += " FROM [" + sel.Table + "] " + + // Add support for BETWEEN x.x + if len(sel.Where) != 0 { + querystr += " WHERE" + for _, loc := range processWhere(sel.Where) { + for _, token := range loc.Expr { + switch token.Type { + case "substitute": + substituteCount++ + querystr += " ?" + strconv.Itoa(substituteCount) + case "function", "operator", "number": + // TODO: Split the function case off to speed things up + if strings.ToUpper(token.Contents) == "UTC_TIMESTAMP()" { + token.Contents = "GETUTCDATE()" + } + querystr += " " + token.Contents + case "column": + querystr += " [" + token.Contents + "]" + case "string": + querystr += " '" + token.Contents + "'" + default: + panic("This token doesn't exist o_o") + } + } + querystr += " AND" + } + querystr = querystr[0 : len(querystr)-4] + } + + // TODO: MSSQL requires ORDER BY for LIMIT + if len(sel.Orderby) != 0 { + querystr += " ORDER BY " + for _, column := range processOrderby(sel.Orderby) { + // TODO: We might want to escape this column + querystr += column.Column + " " + strings.ToUpper(column.Order) + "," + } + querystr = querystr[0 : len(querystr)-1] + } else if sel.Limit != "" { + key, ok := adapter.keys[sel.Table] + if ok { + querystr += " ORDER BY [" + sel.Table + "].[" + key + "]" + } + } + + if sel.Limit != "" { + limiter := processLimit(sel.Limit) + if limiter.Offset != "" { + if limiter.Offset == "?" { + substituteCount++ + querystr += " OFFSET ?" + strconv.Itoa(substituteCount) + " ROWS" + } else { + querystr += " OFFSET " + limiter.Offset + " ROWS" + } + } + + // ! Does this work without an offset? + if limiter.MaxCount != "" { + if limiter.MaxCount == "?" { + substituteCount++ + limiter.MaxCount = "?" + strconv.Itoa(substituteCount) + } + querystr += " FETCH NEXT " + limiter.MaxCount + " ROWS ONLY " + } + } + + querystr = strings.TrimSpace(querystr) + // TODO: Run this on debug mode? + if name[0] == '_' && sel.Limit == "" { + log.Print(name+" query: ", querystr) + } + + adapter.pushStatement(name, "insert", querystr) + return querystr, nil +} + +func (adapter *Mssql_Adapter) simpleJoin(name string, ins DB_Insert, sel DB_Join, joinType string) (string, error) { + // TODO: More errors. + // TODO: Add this to the MySQL adapter in order to make this problem more discoverable? + if len(sel.Orderby) == 0 && sel.Limit != "" { + return "", errors.New("Orderby needs to be set to use limit on Mssql") + } + + /* Insert */ + var querystr = "INSERT INTO [" + ins.Table + "] (" + + // Escape the column names, just in case we've used a reserved keyword + for _, column := range processColumns(ins.Columns) { + if column.Type == "function" { + querystr += column.Left + "," + } else { + querystr += "[" + column.Left + "]," + } + } + querystr = querystr[0:len(querystr)-1] + ") SELECT " + + /* Select */ + var substituteCount = 0 + + for _, column := range processColumns(sel.Columns) { + var source, alias string + + // Escape the column names, just in case we've used a reserved keyword + if column.Table != "" { + source = "[" + column.Table + "].[" + column.Left + "]" + } else if column.Type == "function" { + source = column.Left + } else { + source = "[" + column.Left + "]" + } + + if column.Alias != "" { + alias = " AS '" + column.Alias + "'" + } + querystr += source + alias + "," + } + // Remove the trailing comma + querystr = querystr[0 : len(querystr)-1] + + querystr += " FROM [" + sel.Table1 + "] " + joinType + " JOIN [" + sel.Table2 + "] ON " + for _, joiner := range processJoiner(sel.Joiners) { + querystr += "[" + joiner.LeftTable + "].[" + joiner.LeftColumn + "] " + joiner.Operator + " [" + joiner.RightTable + "].[" + joiner.RightColumn + "] AND " + } + // Remove the trailing AND + querystr = querystr[0 : len(querystr)-4] + + // Add support for BETWEEN x.x + if len(sel.Where) != 0 { + querystr += " WHERE" + for _, loc := range processWhere(sel.Where) { + for _, token := range loc.Expr { + switch token.Type { + case "substitute": + substituteCount++ + querystr += " ?" + strconv.Itoa(substituteCount) + case "function", "operator", "number": + // TODO: Split the function case off to speed things up + if strings.ToUpper(token.Contents) == "UTC_TIMESTAMP()" { + token.Contents = "GETUTCDATE()" + } + querystr += " " + token.Contents + case "column": + halves := strings.Split(token.Contents, ".") + if len(halves) == 2 { + querystr += " [" + halves[0] + "].[" + halves[1] + "]" + } else { + querystr += " [" + token.Contents + "]" + } + case "string": + querystr += " '" + token.Contents + "'" + default: + panic("This token doesn't exist o_o") + } + } + querystr += " AND" + } + querystr = querystr[0 : len(querystr)-4] + } + + // TODO: MSSQL requires ORDER BY for LIMIT + if len(sel.Orderby) != 0 { + querystr += " ORDER BY " + for _, column := range processOrderby(sel.Orderby) { + log.Print("column: ", column) + // TODO: We might want to escape this column + querystr += column.Column + " " + strings.ToUpper(column.Order) + "," + } + querystr = querystr[0 : len(querystr)-1] + } else if sel.Limit != "" { + key, ok := adapter.keys[sel.Table1] + if ok { + querystr += " ORDER BY [" + sel.Table1 + "].[" + key + "]" + } + } + + if sel.Limit != "" { + limiter := processLimit(sel.Limit) + if limiter.Offset != "" { + if limiter.Offset == "?" { + substituteCount++ + querystr += " OFFSET ?" + strconv.Itoa(substituteCount) + " ROWS" + } else { + querystr += " OFFSET " + limiter.Offset + " ROWS" + } + } + + // ! Does this work without an offset? + if limiter.MaxCount != "" { + if limiter.MaxCount == "?" { + substituteCount++ + limiter.MaxCount = "?" + strconv.Itoa(substituteCount) + } + querystr += " FETCH NEXT " + limiter.MaxCount + " ROWS ONLY " + } + } + + querystr = strings.TrimSpace(querystr) + // TODO: Run this on debug mode? + if name[0] == '_' && sel.Limit == "" { + log.Print(name+" query: ", querystr) + } + + adapter.pushStatement(name, "insert", querystr) + return querystr, nil +} + +func (adapter *Mssql_Adapter) SimpleInsertLeftJoin(name string, ins DB_Insert, sel DB_Join) (string, error) { + return adapter.simpleJoin(name, ins, sel, "LEFT") +} + +func (adapter *Mssql_Adapter) SimpleInsertInnerJoin(name string, ins DB_Insert, sel DB_Join) (string, error) { + return adapter.simpleJoin(name, ins, sel, "INNER") +} + +func (adapter *Mssql_Adapter) SimpleCount(name string, table string, where string, limit string) (string, error) { + if name == "" { + return "", errors.New("You need a name for this statement") + } + if table == "" { + return "", errors.New("You need a name for this table") + } + + var querystr = "SELECT COUNT(*) AS [count] FROM [" + table + "]" + + // TODO: Add support for BETWEEN x.x + if len(where) != 0 { + querystr += " WHERE" + //fmt.Println("SimpleCount:",name) + //fmt.Println("where:",where) + //fmt.Println("processWhere:",processWhere(where)) + for _, loc := range processWhere(where) { + for _, token := range loc.Expr { + switch token.Type { + case "function", "operator", "number", "substitute": + if strings.ToUpper(token.Contents) == "UTC_TIMESTAMP()" { + token.Contents = "GETUTCDATE()" + } + querystr += " " + token.Contents + case "column": + querystr += " [" + token.Contents + "]" + case "string": + querystr += " '" + token.Contents + "'" + default: + panic("This token doesn't exist o_o") + } + } + querystr += " AND" + } + querystr = querystr[0 : len(querystr)-4] + } + + if limit != "" { + querystr += " LIMIT " + limit + } + + querystr = strings.TrimSpace(querystr) + adapter.pushStatement(name, "select", querystr) + return querystr, nil +} + +func (adapter *Mssql_Adapter) Write() error { + var stmts, body string + for _, name := range adapter.BufferOrder { + if name[0] == '_' { + continue + } + stmt := adapter.Buffer[name] + // TODO: Add support for create-table? Table creation might be a little complex for Go to do outside a SQL file :( + if stmt.Type != "create-table" { + stmts += "var " + name + "Stmt *sql.Stmt\n" + body += ` + log.Print("Preparing ` + name + ` statement.") + ` + name + `Stmt, err = db.Prepare("` + stmt.Contents + `") + if err != nil { + log.Print("Bad Query: ","` + stmt.Contents + `") + return err + } + ` + } + } + + out := `// +build mssql + +// This file was generated by Gosora's Query Generator. Please try to avoid modifying this file, as it might change at any time. +package main + +import "log" +import "database/sql" + +// nolint +` + stmts + ` +// nolint +func _gen_mssql() (err error) { + if dev.DebugMode { + log.Print("Building the generated statements") + } +` + body + ` + return nil +} +` + return writeFile("./gen_mssql.go", out) +} + +// Internal methods, not exposed in the interface +func (adapter *Mssql_Adapter) pushStatement(name string, stype string, querystr string) { + adapter.Buffer[name] = DB_Stmt{querystr, stype} + adapter.BufferOrder = append(adapter.BufferOrder, name) +} + +func (adapter *Mssql_Adapter) stringyType(ctype string) bool { + ctype = strings.ToLower(ctype) + return ctype == "char" || ctype == "varchar" || ctype == "datetime" || ctype == "text" || ctype == "nvarchar" +} + +type SetPrimaryKeys interface { + SetPrimaryKeys(keys map[string]string) +} + +func (adapter *Mssql_Adapter) SetPrimaryKeys(keys map[string]string) { + adapter.keys = keys +} diff --git a/query_gen/lib/mysql.go b/query_gen/lib/mysql.go index f39e95fd..88f13154 100644 --- a/query_gen/lib/mysql.go +++ b/query_gen/lib/mysql.go @@ -149,6 +149,7 @@ func (adapter *Mysql_Adapter) SimpleInsert(name string, table string, columns st return querystr + ")", nil } +// ! DEPRECATED func (adapter *Mysql_Adapter) SimpleReplace(name string, table string, columns string, fields string) (string, error) { if name == "" { return "", errors.New("You need a name for this statement") @@ -186,6 +187,53 @@ func (adapter *Mysql_Adapter) SimpleReplace(name string, table string, columns s return querystr + ")", nil } +func (adapter *Mysql_Adapter) SimpleUpsert(name string, table string, columns string, fields string, where string) (string, error) { + if name == "" { + return "", errors.New("You need a name for this statement") + } + if table == "" { + return "", errors.New("You need a name for this table") + } + if len(columns) == 0 { + return "", errors.New("No columns found for SimpleInsert") + } + if len(fields) == 0 { + return "", errors.New("No input data found for SimpleInsert") + } + if where == "" { + return "", errors.New("You need a where for this upsert") + } + + var querystr = "INSERT INTO `" + table + "`(" + var parsedFields = processFields(fields) + + var insertColumns string + var insertValues string + var setBit = ") ON DUPLICATE KEY UPDATE " + + for columnID, column := range processColumns(columns) { + field := parsedFields[columnID] + if column.Type == "function" { + insertColumns += column.Left + "," + insertValues += field.Name + "," + setBit += column.Left + " = " + field.Name + " AND " + } else { + insertColumns += "`" + column.Left + "`," + insertValues += field.Name + "," + setBit += "`" + column.Left + "` = " + field.Name + " AND " + } + } + insertColumns = insertColumns[0 : len(insertColumns)-1] + insertValues = insertValues[0 : len(insertValues)-1] + insertColumns += ") VALUES (" + insertValues + setBit = setBit[0 : len(setBit)-5] + + querystr += insertColumns + setBit + + adapter.pushStatement(name, "upsert", querystr) + return querystr, nil +} + func (adapter *Mysql_Adapter) SimpleUpdate(name string, table string, set string, where string) (string, error) { if name == "" { return "", errors.New("You need a name for this statement") @@ -212,7 +260,6 @@ func (adapter *Mysql_Adapter) SimpleUpdate(name string, table string, set string } querystr += "," } - // Remove the trailing comma querystr = querystr[0 : len(querystr)-1] @@ -826,8 +873,17 @@ func (adapter *Mysql_Adapter) Write() error { continue } stmt := adapter.Buffer[name] - // TODO: Add support for create-table? Table creation might be a little complex for Go to do outside a SQL file :( - if stmt.Type != "create-table" { + // ? - Table creation might be a little complex for Go to do outside a SQL file :( + if stmt.Type == "upsert" { + stmts += "var " + name + "Stmt *qgen.MySQLUpsertCallback\n" + body += ` + log.Print("Preparing ` + name + ` statement.") + ` + name + `Stmt, err = qgen.PrepareMySQLUpsertCallback(db, "` + stmt.Contents + `") + if err != nil { + return err + } + ` + } else if stmt.Type != "create-table" { stmts += "var " + name + "Stmt *sql.Stmt\n" body += ` log.Print("Preparing ` + name + ` statement.") @@ -847,6 +903,7 @@ package main import "log" import "database/sql" +import "./query_gen/lib" // nolint ` + stmts + ` diff --git a/query_gen/lib/pgsql.go b/query_gen/lib/pgsql.go index e1214291..0af25936 100644 --- a/query_gen/lib/pgsql.go +++ b/query_gen/lib/pgsql.go @@ -128,6 +128,23 @@ func (adapter *Pgsql_Adapter) SimpleReplace(name string, table string, columns s return "", nil } +// TODO: Implement this +func (adapter *Pgsql_Adapter) SimpleUpsert(name string, table string, columns string, fields string, where string) (string, error) { + if name == "" { + return "", errors.New("You need a name for this statement") + } + if table == "" { + return "", errors.New("You need a name for this table") + } + if len(columns) == 0 { + return "", errors.New("No columns found for SimpleInsert") + } + if len(fields) == 0 { + return "", errors.New("No input data found for SimpleInsert") + } + return "", nil +} + // TODO: Implemented, but we need CreateTable and a better installer to *test* it func (adapter *Pgsql_Adapter) SimpleUpdate(name string, table string, set string, where string) (string, error) { if name == "" { diff --git a/query_gen/lib/querygen.go b/query_gen/lib/querygen.go index fac57e1f..cfc06ac4 100644 --- a/query_gen/lib/querygen.go +++ b/query_gen/lib/querygen.go @@ -1,10 +1,13 @@ /* WIP Under Construction */ package qgen -import "errors" +import ( + "database/sql" + "errors" +) var DB_Registry []DB_Adapter -var No_Adapter = errors.New("This adapter doesn't exist") +var ErrNoAdapter = errors.New("This adapter doesn't exist") type DB_Table_Column struct { Name string @@ -97,7 +100,12 @@ type DB_Adapter interface { GetName() string CreateTable(name string, table string, charset string, collation string, columns []DB_Table_Column, keys []DB_Table_Key) (string, error) SimpleInsert(name string, table string, columns string, fields string) (string, error) + + // ! DEPRECATED SimpleReplace(name string, table string, columns string, fields string) (string, error) + + // ! NOTE: MySQL doesn't support upserts properly, asides from for keys, so this is just a less destructive replace atm + SimpleUpsert(name string, table string, columns string, fields string, where string) (string, error) SimpleUpdate(name string, table string, set string, where string) (string, error) SimpleDelete(name string, table string, where string) (string, error) Purge(name string, table string) (string, error) @@ -119,5 +127,27 @@ func GetAdapter(name string) (adap DB_Adapter, err error) { return adapter, nil } } - return adap, No_Adapter + return adap, ErrNoAdapter +} + +type QueryPlugin interface { + Hook(name string, args ...interface{}) error + Write() error +} + +type MySQLUpsertCallback struct { + stmt *sql.Stmt +} + +func (double *MySQLUpsertCallback) Exec(args ...interface{}) (res sql.Result, err error) { + if len(args) < 2 { + return res, errors.New("Need two or more arguments") + } + args = args[:len(args)-1] + return double.stmt.Exec(append(args, args...)...) +} + +func PrepareMySQLUpsertCallback(db *sql.DB, query string) (*MySQLUpsertCallback, error) { + stmt, err := db.Prepare(query) + return &MySQLUpsertCallback{stmt}, err } diff --git a/query_gen/lib/utils.go b/query_gen/lib/utils.go index bab8ffbd..fd0196a7 100644 --- a/query_gen/lib/utils.go +++ b/query_gen/lib/utils.go @@ -8,8 +8,10 @@ package qgen //import "fmt" -import "strings" -import "os" +import ( + "os" + "strings" +) func processColumns(colstr string) (columns []DB_Column) { if colstr == "" { @@ -46,6 +48,7 @@ func processColumns(colstr string) (columns []DB_Column) { return columns } +// TODO: Allow order by statements without a direction func processOrderby(orderstr string) (order []DB_Order) { if orderstr == "" { return order diff --git a/query_gen/main.go b/query_gen/main.go index 53595876..5c73642c 100644 --- a/query_gen/main.go +++ b/query_gen/main.go @@ -1,83 +1,116 @@ /* WIP Under Construction */ package main -import "log" -import "./lib" +import ( + "fmt" + "log" + "os" + "runtime/debug" + "./lib" +) + +// TODO: Make sure all the errors in this file propagate upwards properly func main() { + // Capture panics instead of closing the window at a superhuman speed before the user can read the message on Windows + defer func() { + r := recover() + if r != nil { + fmt.Println(r) + debug.PrintStack() + return + } + }() + log.Println("Running the query generator") for _, adapter := range qgen.DB_Registry { log.Println("Building the queries for the " + adapter.GetName() + " adapter") qgen.Install.SetAdapterInstance(adapter) - write_statements(adapter) - qgen.Install.Write() - adapter.Write() + qgen.Install.RegisterPlugin(NewPrimaryKeySpitter()) // TODO: Do we really need to fill the spitter for every adapter? + + err := writeStatements(adapter) + if err != nil { + log.Print(err) + } + err = qgen.Install.Write() + if err != nil { + log.Print(err) + } + err = adapter.Write() + if err != nil { + log.Print(err) + } } } // nolint -func write_statements(adapter qgen.DB_Adapter) error { - err := create_tables(adapter) +func writeStatements(adapter qgen.DB_Adapter) error { + err := createTables(adapter) if err != nil { return err } - err = seed_tables(adapter) + err = seedTables(adapter) if err != nil { return err } - err = write_selects(adapter) + err = writeSelects(adapter) if err != nil { return err } - err = write_left_joins(adapter) + err = writeLeftJoins(adapter) if err != nil { return err } - err = write_inner_joins(adapter) + err = writeInnerJoins(adapter) if err != nil { return err } - err = write_inserts(adapter) + err = writeInserts(adapter) if err != nil { return err } - err = write_replaces(adapter) + err = writeReplaces(adapter) if err != nil { return err } - err = write_updates(adapter) + err = writeUpserts(adapter) if err != nil { return err } - err = write_deletes(adapter) + err = writeUpdates(adapter) if err != nil { return err } - err = write_simple_counts(adapter) + err = writeDeletes(adapter) if err != nil { return err } - err = write_insert_selects(adapter) + err = writeSimpleCounts(adapter) if err != nil { return err } - err = write_insert_left_joins(adapter) + err = writeInsertSelects(adapter) if err != nil { return err } - err = write_insert_inner_joins(adapter) + err = writeInsertLeftJoins(adapter) + if err != nil { + return err + } + + err = writeInsertInnerJoins(adapter) if err != nil { return err } @@ -85,368 +118,7 @@ func write_statements(adapter qgen.DB_Adapter) error { return nil } -// nolint -func create_tables(adapter qgen.DB_Adapter) error { - qgen.Install.CreateTable("users", "utf8mb4", "utf8mb4_general_ci", - []qgen.DB_Table_Column{ - qgen.DB_Table_Column{"uid", "int", 0, false, true, ""}, - qgen.DB_Table_Column{"name", "varchar", 100, false, false, ""}, - qgen.DB_Table_Column{"password", "varchar", 100, false, false, ""}, - qgen.DB_Table_Column{"salt", "varchar", 80, false, false, "''"}, - qgen.DB_Table_Column{"group", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"active", "boolean", 0, false, false, "0"}, - qgen.DB_Table_Column{"is_super_admin", "boolean", 0, false, false, "0"}, - qgen.DB_Table_Column{"createdAt", "createdAt", 0, false, false, ""}, - qgen.DB_Table_Column{"lastActiveAt", "datetime", 0, false, false, ""}, - qgen.DB_Table_Column{"session", "varchar", 200, false, false, "''"}, - qgen.DB_Table_Column{"last_ip", "varchar", 200, false, false, "0.0.0.0.0"}, - qgen.DB_Table_Column{"email", "varchar", 200, false, false, "''"}, - qgen.DB_Table_Column{"avatar", "varchar", 100, false, false, "''"}, - qgen.DB_Table_Column{"message", "text", 0, false, false, "''"}, - qgen.DB_Table_Column{"url_prefix", "varchar", 20, false, false, "''"}, - qgen.DB_Table_Column{"url_name", "varchar", 100, false, false, "''"}, - qgen.DB_Table_Column{"level", "smallint", 0, false, false, "0"}, - qgen.DB_Table_Column{"score", "int", 0, false, false, "0"}, - qgen.DB_Table_Column{"posts", "int", 0, false, false, "0"}, - qgen.DB_Table_Column{"bigposts", "int", 0, false, false, "0"}, - qgen.DB_Table_Column{"megaposts", "int", 0, false, false, "0"}, - qgen.DB_Table_Column{"topics", "int", 0, false, false, "0"}, - //qgen.DB_Table_Column{"penalty_count","int",0,false,false,"0"}, - qgen.DB_Table_Column{"temp_group", "int", 0, false, false, "0"}, // For temporary groups, set this to zero when a temporary group isn't in effect - }, - []qgen.DB_Table_Key{ - qgen.DB_Table_Key{"uid", "primary"}, - qgen.DB_Table_Key{"name", "unique"}, - }, - ) - - qgen.Install.CreateTable("users_groups", "utf8mb4", "utf8mb4_general_ci", - []qgen.DB_Table_Column{ - qgen.DB_Table_Column{"gid", "int", 0, false, true, ""}, - qgen.DB_Table_Column{"name", "varchar", 100, false, false, ""}, - qgen.DB_Table_Column{"permissions", "text", 0, false, false, ""}, - qgen.DB_Table_Column{"plugin_perms", "text", 0, false, false, ""}, - qgen.DB_Table_Column{"is_mod", "boolean", 0, false, false, "0"}, - qgen.DB_Table_Column{"is_admin", "boolean", 0, false, false, "0"}, - qgen.DB_Table_Column{"is_banned", "boolean", 0, false, false, "0"}, - qgen.DB_Table_Column{"tag", "varchar", 50, false, false, "''"}, - }, - []qgen.DB_Table_Key{ - qgen.DB_Table_Key{"gid", "primary"}, - }, - ) - - // What should we do about global penalties? Put them on the users table for speed? Or keep them here? - // Should we add IP Penalties? No, that's a stupid idea, just implement IP Bans properly. What about shadowbans? - // TODO: Perm overrides - // TODO: Add a mod-queue and other basic auto-mod features. This is needed for awaiting activation and the mod_queue penalty flag - // TODO: Add a penalty type where a user is stopped from creating plugin_socialgroups social groups - // TODO: Shadow bans. We will probably have a CanShadowBan permission for this, as we *really* don't want people using this lightly. - /*qgen.Install.CreateTable("users_penalties","","", - []qgen.DB_Table_Column{ - qgen.DB_Table_Column{"uid","int",0,false,false,""}, - qgen.DB_Table_Column{"element_id","int",0,false,false,""}, - qgen.DB_Table_Column{"element_type","varchar",50,false,false,""}, //forum, profile?, and social_group. Leave blank for global. - qgen.DB_Table_Column{"overrides","text",0,false,false,"{}"}, - - qgen.DB_Table_Column{"mod_queue","boolean",0,false,false,"0"}, - qgen.DB_Table_Column{"shadow_ban","boolean",0,false,false,"0"}, - qgen.DB_Table_Column{"no_avatar","boolean",0,false,false,"0"}, // Coming Soon. Should this be a perm override instead? - - // Do we *really* need rate-limit penalty types? Are we going to be allowing bots or something? - //qgen.DB_Table_Column{"posts_per_hour","int",0,false,false,"0"}, - //qgen.DB_Table_Column{"topics_per_hour","int",0,false,false,"0"}, - //qgen.DB_Table_Column{"posts_count","int",0,false,false,"0"}, - //qgen.DB_Table_Column{"topic_count","int",0,false,false,"0"}, - //qgen.DB_Table_Column{"last_hour","int",0,false,false,"0"}, // UNIX Time, as we don't need to do anything too fancy here. When an hour has elapsed since that time, reset the hourly penalty counters. - - qgen.DB_Table_Column{"issued_by","int",0,false,false,""}, - qgen.DB_Table_Column{"issued_at","createdAt",0,false,false,""}, - qgen.DB_Table_Column{"expires_at","datetime",0,false,false,""}, - }, - []qgen.DB_Table_Key{}, - )*/ - - qgen.Install.CreateTable("users_groups_scheduler", "", "", - []qgen.DB_Table_Column{ - qgen.DB_Table_Column{"uid", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"set_group", "int", 0, false, false, ""}, - - qgen.DB_Table_Column{"issued_by", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"issued_at", "createdAt", 0, false, false, ""}, - qgen.DB_Table_Column{"revert_at", "datetime", 0, false, false, ""}, - qgen.DB_Table_Column{"temporary", "boolean", 0, false, false, ""}, // special case for permanent bans to do the necessary bookkeeping, might be removed in the future - }, - []qgen.DB_Table_Key{ - qgen.DB_Table_Key{"uid", "primary"}, - }, - ) - - qgen.Install.CreateTable("emails", "", "", - []qgen.DB_Table_Column{ - qgen.DB_Table_Column{"email", "varchar", 200, false, false, ""}, - qgen.DB_Table_Column{"uid", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"validated", "boolean", 0, false, false, "0"}, - qgen.DB_Table_Column{"token", "varchar", 200, false, false, "''"}, - }, - []qgen.DB_Table_Key{}, - ) - - qgen.Install.CreateTable("forums", "utf8mb4", "utf8mb4_general_ci", - []qgen.DB_Table_Column{ - qgen.DB_Table_Column{"fid", "int", 0, false, true, ""}, - qgen.DB_Table_Column{"name", "varchar", 100, false, false, ""}, - qgen.DB_Table_Column{"desc", "varchar", 200, false, false, ""}, - qgen.DB_Table_Column{"active", "boolean", 0, false, false, "1"}, - qgen.DB_Table_Column{"topicCount", "int", 0, false, false, "0"}, - qgen.DB_Table_Column{"preset", "varchar", 100, false, false, "''"}, - qgen.DB_Table_Column{"parentID", "int", 0, false, false, "0"}, - qgen.DB_Table_Column{"parentType", "varchar", 50, false, false, "''"}, - qgen.DB_Table_Column{"lastTopicID", "int", 0, false, false, "0"}, - qgen.DB_Table_Column{"lastReplyerID", "int", 0, false, false, "0"}, - }, - []qgen.DB_Table_Key{ - qgen.DB_Table_Key{"fid", "primary"}, - }, - ) - - qgen.Install.CreateTable("forums_permissions", "", "", - []qgen.DB_Table_Column{ - qgen.DB_Table_Column{"fid", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"gid", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"preset", "varchar", 100, false, false, "''"}, - qgen.DB_Table_Column{"permissions", "text", 0, false, false, ""}, - }, - []qgen.DB_Table_Key{ - // TODO: Test to see that the compound primary key works - qgen.DB_Table_Key{"fid,gid", "primary"}, - }, - ) - - qgen.Install.CreateTable("topics", "utf8mb4", "utf8mb4_general_ci", - []qgen.DB_Table_Column{ - qgen.DB_Table_Column{"tid", "int", 0, false, true, ""}, - qgen.DB_Table_Column{"title", "varchar", 100, false, false, ""}, - qgen.DB_Table_Column{"content", "text", 0, false, false, ""}, - qgen.DB_Table_Column{"parsed_content", "text", 0, false, false, ""}, - qgen.DB_Table_Column{"createdAt", "createdAt", 0, false, false, ""}, - qgen.DB_Table_Column{"lastReplyAt", "datetime", 0, false, false, ""}, - qgen.DB_Table_Column{"lastReplyBy", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"createdBy", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"is_closed", "boolean", 0, false, false, "0"}, - qgen.DB_Table_Column{"sticky", "boolean", 0, false, false, "0"}, - qgen.DB_Table_Column{"parentID", "int", 0, false, false, "2"}, - qgen.DB_Table_Column{"ipaddress", "varchar", 200, false, false, "0.0.0.0.0"}, - qgen.DB_Table_Column{"postCount", "int", 0, false, false, "1"}, - qgen.DB_Table_Column{"likeCount", "int", 0, false, false, "0"}, - qgen.DB_Table_Column{"words", "int", 0, false, false, "0"}, - qgen.DB_Table_Column{"css_class", "varchar", 100, false, false, "''"}, - qgen.DB_Table_Column{"data", "varchar", 200, false, false, "''"}, - }, - []qgen.DB_Table_Key{ - qgen.DB_Table_Key{"tid", "primary"}, - }, - ) - - qgen.Install.CreateTable("replies", "utf8mb4", "utf8mb4_general_ci", - []qgen.DB_Table_Column{ - qgen.DB_Table_Column{"rid", "int", 0, false, true, ""}, - qgen.DB_Table_Column{"tid", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"content", "text", 0, false, false, ""}, - qgen.DB_Table_Column{"parsed_content", "text", 0, false, false, ""}, - qgen.DB_Table_Column{"createdAt", "createdAt", 0, false, false, ""}, - qgen.DB_Table_Column{"createdBy", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"lastEdit", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"lastEditBy", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"lastUpdated", "datetime", 0, false, false, ""}, - qgen.DB_Table_Column{"ipaddress", "varchar", 200, false, false, "0.0.0.0.0"}, - qgen.DB_Table_Column{"likeCount", "int", 0, false, false, "0"}, - qgen.DB_Table_Column{"words", "int", 0, false, false, "1"}, // ? - replies has a default of 1 and topics has 0? why? - qgen.DB_Table_Column{"actionType", "varchar", 20, false, false, "''"}, - }, - []qgen.DB_Table_Key{ - qgen.DB_Table_Key{"rid", "primary"}, - }, - ) - - qgen.Install.CreateTable("attachments", "utf8mb4", "utf8mb4_general_ci", - []qgen.DB_Table_Column{ - qgen.DB_Table_Column{"attachID", "int", 0, false, true, ""}, - qgen.DB_Table_Column{"sectionID", "int", 0, false, false, "0"}, - qgen.DB_Table_Column{"sectionTable", "varchar", 200, false, false, "forums"}, - qgen.DB_Table_Column{"originID", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"originTable", "varchar", 200, false, false, "replies"}, - qgen.DB_Table_Column{"uploadedBy", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"path", "varchar", 200, false, false, ""}, - }, - []qgen.DB_Table_Key{ - qgen.DB_Table_Key{"attachID", "primary"}, - }, - ) - - qgen.Install.CreateTable("revisions", "utf8mb4", "utf8mb4_general_ci", - []qgen.DB_Table_Column{ - qgen.DB_Table_Column{"index", "int", 0, false, false, ""}, // TODO: Replace this with a proper revision ID x.x - qgen.DB_Table_Column{"content", "text", 0, false, false, ""}, - qgen.DB_Table_Column{"contentID", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"contentType", "varchar", 100, false, false, "replies"}, - }, - []qgen.DB_Table_Key{}, - ) - - qgen.Install.CreateTable("users_replies", "utf8mb4", "utf8mb4_general_ci", - []qgen.DB_Table_Column{ - qgen.DB_Table_Column{"rid", "int", 0, false, true, ""}, - qgen.DB_Table_Column{"uid", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"content", "text", 0, false, false, ""}, - qgen.DB_Table_Column{"parsed_content", "text", 0, false, false, ""}, - qgen.DB_Table_Column{"createdAt", "createdAt", 0, false, false, ""}, - qgen.DB_Table_Column{"createdBy", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"lastEdit", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"lastEditBy", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"ipaddress", "varchar", 200, false, false, "0.0.0.0.0"}, - }, - []qgen.DB_Table_Key{ - qgen.DB_Table_Key{"rid", "primary"}, - }, - ) - - qgen.Install.CreateTable("likes", "", "", - []qgen.DB_Table_Column{ - qgen.DB_Table_Column{"weight", "tinyint", 0, false, false, "1"}, - qgen.DB_Table_Column{"targetItem", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"targetType", "varchar", 50, false, false, "replies"}, - qgen.DB_Table_Column{"sentBy", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"recalc", "tinyint", 0, false, false, "0"}, - }, - []qgen.DB_Table_Key{}, - ) - - qgen.Install.CreateTable("activity_stream_matches", "", "", - []qgen.DB_Table_Column{ - qgen.DB_Table_Column{"watcher", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"asid", "int", 0, false, false, ""}, - }, - []qgen.DB_Table_Key{}, - ) - - qgen.Install.CreateTable("activity_stream", "", "", - []qgen.DB_Table_Column{ - qgen.DB_Table_Column{"asid", "int", 0, false, true, ""}, - qgen.DB_Table_Column{"actor", "int", 0, false, false, ""}, /* the one doing the act */ - qgen.DB_Table_Column{"targetUser", "int", 0, false, false, ""}, /* the user who created the item the actor is acting on, some items like forums may lack a targetUser field */ - qgen.DB_Table_Column{"event", "varchar", 50, false, false, ""}, /* mention, like, reply (as in the act of replying to an item, not the reply item type, you can "reply" to a forum by making a topic in it), friend_invite */ - qgen.DB_Table_Column{"elementType", "varchar", 50, false, false, ""}, /* topic, post (calling it post here to differentiate it from the 'reply' event), forum, user */ - qgen.DB_Table_Column{"elementID", "int", 0, false, false, ""}, /* the ID of the element being acted upon */ - }, - []qgen.DB_Table_Key{ - qgen.DB_Table_Key{"asid", "primary"}, - }, - ) - - qgen.Install.CreateTable("activity_subscriptions", "", "", - []qgen.DB_Table_Column{ - qgen.DB_Table_Column{"user", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"targetID", "int", 0, false, false, ""}, /* the ID of the element being acted upon */ - qgen.DB_Table_Column{"targetType", "varchar", 50, false, false, ""}, /* topic, post (calling it post here to differentiate it from the 'reply' event), forum, user */ - qgen.DB_Table_Column{"level", "int", 0, false, false, "0"}, /* 0: Mentions (aka the global default for any post), 1: Replies To You, 2: All Replies*/ - }, - []qgen.DB_Table_Key{}, - ) - - /* Due to MySQL's design, we have to drop the unique keys for table settings, plugins, and themes down from 200 to 180 or it will error */ - qgen.Install.CreateTable("settings", "", "", - []qgen.DB_Table_Column{ - qgen.DB_Table_Column{"name", "varchar", 180, false, false, ""}, - qgen.DB_Table_Column{"content", "varchar", 250, false, false, ""}, - qgen.DB_Table_Column{"type", "varchar", 50, false, false, ""}, - qgen.DB_Table_Column{"constraints", "varchar", 200, false, false, "''"}, - }, - []qgen.DB_Table_Key{ - qgen.DB_Table_Key{"name", "unique"}, - }, - ) - - qgen.Install.CreateTable("word_filters", "", "", - []qgen.DB_Table_Column{ - qgen.DB_Table_Column{"wfid", "int", 0, false, true, ""}, - qgen.DB_Table_Column{"find", "varchar", 200, false, false, ""}, - qgen.DB_Table_Column{"replacement", "varchar", 200, false, false, ""}, - }, - []qgen.DB_Table_Key{ - qgen.DB_Table_Key{"wfid", "primary"}, - }, - ) - - qgen.Install.CreateTable("plugins", "", "", - []qgen.DB_Table_Column{ - qgen.DB_Table_Column{"uname", "varchar", 180, false, false, ""}, - qgen.DB_Table_Column{"active", "boolean", 0, false, false, "0"}, - qgen.DB_Table_Column{"installed", "boolean", 0, false, false, "0"}, - }, - []qgen.DB_Table_Key{ - qgen.DB_Table_Key{"uname", "unique"}, - }, - ) - - qgen.Install.CreateTable("themes", "", "", - []qgen.DB_Table_Column{ - qgen.DB_Table_Column{"uname", "varchar", 180, false, false, ""}, - qgen.DB_Table_Column{"default", "boolean", 0, false, false, "0"}, - }, - []qgen.DB_Table_Key{ - qgen.DB_Table_Key{"uname", "unique"}, - }, - ) - - qgen.Install.CreateTable("widgets", "", "", - []qgen.DB_Table_Column{ - qgen.DB_Table_Column{"position", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"side", "varchar", 100, false, false, ""}, - qgen.DB_Table_Column{"type", "varchar", 100, false, false, ""}, - qgen.DB_Table_Column{"active", "boolean", 0, false, false, "0"}, - qgen.DB_Table_Column{"location", "varchar", 100, false, false, ""}, - qgen.DB_Table_Column{"data", "text", 0, false, false, "''"}, - }, - []qgen.DB_Table_Key{}, - ) - - qgen.Install.CreateTable("moderation_logs", "", "", - []qgen.DB_Table_Column{ - qgen.DB_Table_Column{"action", "varchar", 100, false, false, ""}, - qgen.DB_Table_Column{"elementID", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"elementType", "varchar", 100, false, false, ""}, - qgen.DB_Table_Column{"ipaddress", "varchar", 200, false, false, ""}, - qgen.DB_Table_Column{"actorID", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"doneAt", "datetime", 0, false, false, ""}, - }, - []qgen.DB_Table_Key{}, - ) - - qgen.Install.CreateTable("administration_logs", "", "", - []qgen.DB_Table_Column{ - qgen.DB_Table_Column{"action", "varchar", 100, false, false, ""}, - qgen.DB_Table_Column{"elementID", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"elementType", "varchar", 100, false, false, ""}, - qgen.DB_Table_Column{"ipaddress", "varchar", 200, false, false, ""}, - qgen.DB_Table_Column{"actorID", "int", 0, false, false, ""}, - qgen.DB_Table_Column{"doneAt", "datetime", 0, false, false, ""}, - }, - []qgen.DB_Table_Key{}, - ) - - qgen.Install.CreateTable("sync", "", "", - []qgen.DB_Table_Column{ - qgen.DB_Table_Column{"last_update", "datetime", 0, false, false, ""}, - }, - []qgen.DB_Table_Key{}, - ) - - return nil -} - -// nolint -func seed_tables(adapter qgen.DB_Adapter) error { +func seedTables(adapter qgen.DB_Adapter) error { qgen.Install.SimpleInsert("sync", "last_update", "UTC_TIMESTAMP()") qgen.Install.SimpleInsert("settings", "name, content, type", "'url_tags','1','bool'") @@ -547,8 +219,7 @@ func seed_tables(adapter qgen.DB_Adapter) error { return nil } -// nolint -func write_selects(adapter qgen.DB_Adapter) error { +func writeSelects(adapter qgen.DB_Adapter) error { // url_prefix and url_name will be removed from this query in a later commit adapter.SimpleSelect("getUser", "users", "name, group, is_super_admin, avatar, message, url_prefix, url_name, level", "uid = ?", "", "") @@ -586,7 +257,7 @@ func write_selects(adapter qgen.DB_Adapter) error { adapter.SimpleSelect("getUsers", "users", "uid, name, group, active, is_super_admin, avatar", "", "", "") - adapter.SimpleSelect("getUsersOffset", "users", "uid, name, group, active, is_super_admin, avatar", "", "", "?,?") + adapter.SimpleSelect("getUsersOffset", "users", "uid, name, group, active, is_super_admin, avatar", "", "uid ASC", "?,?") adapter.SimpleSelect("getWordFilters", "word_filters", "wfid, find, replacement", "", "", "") @@ -594,7 +265,7 @@ func write_selects(adapter qgen.DB_Adapter) error { adapter.SimpleSelect("getModlogs", "moderation_logs", "action, elementID, elementType, ipaddress, actorID, doneAt", "", "", "") - adapter.SimpleSelect("getModlogsOffset", "moderation_logs", "action, elementID, elementType, ipaddress, actorID, doneAt", "", "", "?,?") + adapter.SimpleSelect("getModlogsOffset", "moderation_logs", "action, elementID, elementType, ipaddress, actorID, doneAt", "", "doneAt DESC", "?,?") adapter.SimpleSelect("getReplyTID", "replies", "tid", "rid = ?", "", "") @@ -629,9 +300,8 @@ func write_selects(adapter qgen.DB_Adapter) error { return nil } -// nolint -func write_left_joins(adapter qgen.DB_Adapter) error { - adapter.SimpleLeftJoin("getTopicRepliesOffset", "replies", "users", "replies.rid, replies.content, replies.createdBy, replies.createdAt, replies.lastEdit, replies.lastEditBy, users.avatar, users.name, users.group, users.url_prefix, users.url_name, users.level, replies.ipaddress, replies.likeCount, replies.actionType", "replies.createdBy = users.uid", "replies.tid = ?", "", "?,?") +func writeLeftJoins(adapter qgen.DB_Adapter) error { + adapter.SimpleLeftJoin("getTopicRepliesOffset", "replies", "users", "replies.rid, replies.content, replies.createdBy, replies.createdAt, replies.lastEdit, replies.lastEditBy, users.avatar, users.name, users.group, users.url_prefix, users.url_name, users.level, replies.ipaddress, replies.likeCount, replies.actionType", "replies.createdBy = users.uid", "replies.tid = ?", "replies.rid ASC", "?,?") adapter.SimpleLeftJoin("getTopicList", "topics", "users", "topics.tid, topics.title, topics.content, topics.createdBy, topics.is_closed, topics.sticky, topics.createdAt, topics.parentID, users.name, users.avatar", "topics.createdBy = users.uid", "", "topics.sticky DESC, topics.lastReplyAt DESC, topics.createdBy DESC", "") @@ -648,22 +318,23 @@ func write_left_joins(adapter qgen.DB_Adapter) error { return nil } -// nolint -func write_inner_joins(adapter qgen.DB_Adapter) error { - adapter.SimpleInnerJoin("getWatchers", "activity_stream", "activity_subscriptions", "activity_subscriptions.user", "activity_subscriptions.targetType = activity_stream.elementType AND activity_subscriptions.targetID = activity_stream.elementID AND activity_subscriptions.user != activity_stream.actor", "asid = ?", "", "") +func writeInnerJoins(adapter qgen.DB_Adapter) (err error) { + _, err = adapter.SimpleInnerJoin("getWatchers", "activity_stream", "activity_subscriptions", "activity_subscriptions.user", "activity_subscriptions.targetType = activity_stream.elementType AND activity_subscriptions.targetID = activity_stream.elementID AND activity_subscriptions.user != activity_stream.actor", "asid = ?", "", "") + if err != nil { + return err + } return nil } -// nolint -func write_inserts(adapter qgen.DB_Adapter) error { +func writeInserts(adapter qgen.DB_Adapter) error { adapter.SimpleInsert("createTopic", "topics", "parentID, title, content, parsed_content, createdAt, lastReplyAt, lastReplyBy, ipaddress, words, createdBy", "?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?,?") - adapter.SimpleInsert("createReport", "topics", "title, content, parsed_content, createdAt, lastReplyAt, createdBy, data, parentID, css_class", "?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,1,'report'") + adapter.SimpleInsert("createReport", "topics", "title, content, parsed_content, createdAt, lastReplyAt, createdBy, lastReplyBy, data, parentID, css_class", "?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?,1,'report'") - adapter.SimpleInsert("createReply", "replies", "tid, content, parsed_content, createdAt, ipaddress, words, createdBy", "?,?,?,UTC_TIMESTAMP(),?,?,?") + adapter.SimpleInsert("createReply", "replies", "tid, content, parsed_content, createdAt, lastUpdated, ipaddress, words, createdBy", "?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?") - adapter.SimpleInsert("createActionReply", "replies", "tid, actionType, ipaddress, createdBy", "?,?,?,?") + adapter.SimpleInsert("createActionReply", "replies", "tid, actionType, ipaddress, createdBy, createdAt, lastUpdated, content, parsed_content", "?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),'',''") adapter.SimpleInsert("createLike", "likes", "weight, targetItem, targetType, sentBy", "?,?,?,?") @@ -698,17 +369,25 @@ func write_inserts(adapter qgen.DB_Adapter) error { return nil } -// nolint -func write_replaces(adapter qgen.DB_Adapter) error { - adapter.SimpleReplace("addForumPermsToGroup", "forums_permissions", "gid, fid, preset, permissions", "?,?,?,?") +func writeReplaces(adapter qgen.DB_Adapter) (err error) { + return nil +} - adapter.SimpleReplace("replaceScheduleGroup", "users_groups_scheduler", "uid, set_group, issued_by, issued_at, revert_at, temporary", "?,?,?,UTC_TIMESTAMP(),?,?") +func writeUpserts(adapter qgen.DB_Adapter) (err error) { + _, err = adapter.SimpleUpsert("addForumPermsToGroup", "forums_permissions", "gid, fid, preset, permissions", "?,?,?,?", "gid = ? AND fid = ?") + if err != nil { + return err + } + + _, err = adapter.SimpleUpsert("replaceScheduleGroup", "users_groups_scheduler", "uid, set_group, issued_by, issued_at, revert_at, temporary", "?,?,?,UTC_TIMESTAMP(),?,?", "uid = ?") + if err != nil { + return err + } return nil } -// nolint -func write_updates(adapter qgen.DB_Adapter) error { +func writeUpdates(adapter qgen.DB_Adapter) error { adapter.SimpleUpdate("addRepliesToTopic", "topics", "postCount = postCount + ?, lastReplyBy = ?, lastReplyAt = UTC_TIMESTAMP()", "tid = ?") adapter.SimpleUpdate("removeRepliesFromTopic", "topics", "postCount = postCount - ?", "tid = ?") @@ -794,8 +473,9 @@ func write_updates(adapter qgen.DB_Adapter) error { return nil } -// nolint -func write_deletes(adapter qgen.DB_Adapter) error { +func writeDeletes(adapter qgen.DB_Adapter) error { + adapter.SimpleDelete("deleteUser", "users", "uid = ?") + adapter.SimpleDelete("deleteReply", "replies", "rid = ?") adapter.SimpleDelete("deleteProfileReply", "users_replies", "rid = ?") @@ -810,8 +490,7 @@ func write_deletes(adapter qgen.DB_Adapter) error { return nil } -// nolint -func write_simple_counts(adapter qgen.DB_Adapter) error { +func writeSimpleCounts(adapter qgen.DB_Adapter) error { adapter.SimpleCount("reportExists", "topics", "data = ? AND data != '' AND parentID = 1", "") adapter.SimpleCount("groupCount", "users_groups", "", "") @@ -821,8 +500,7 @@ func write_simple_counts(adapter qgen.DB_Adapter) error { return nil } -// nolint -func write_insert_selects(adapter qgen.DB_Adapter) error { +func writeInsertSelects(adapter qgen.DB_Adapter) error { adapter.SimpleInsertSelect("addForumPermsToForumAdmins", qgen.DB_Insert{"forums_permissions", "gid, fid, preset, permissions", ""}, qgen.DB_Select{"users_groups", "gid, ? AS fid, ? AS preset, ? AS permissions", "is_admin = 1", "", ""}, @@ -842,12 +520,11 @@ func write_insert_selects(adapter qgen.DB_Adapter) error { } // nolint -func write_insert_left_joins(adapter qgen.DB_Adapter) error { +func writeInsertLeftJoins(adapter qgen.DB_Adapter) error { return nil } -// nolint -func write_insert_inner_joins(adapter qgen.DB_Adapter) error { +func writeInsertInnerJoins(adapter qgen.DB_Adapter) error { adapter.SimpleInsertInnerJoin("notifyWatchers", qgen.DB_Insert{"activity_stream_matches", "watcher, asid", ""}, qgen.DB_Join{"activity_stream", "activity_subscriptions", "activity_subscriptions.user, activity_stream.asid", "activity_subscriptions.targetType = activity_stream.elementType AND activity_subscriptions.targetID = activity_stream.elementID AND activity_subscriptions.user != activity_stream.actor", "asid = ?", "", ""}, @@ -855,3 +532,19 @@ func write_insert_inner_joins(adapter qgen.DB_Adapter) error { return nil } + +func writeFile(name string, content string) (err error) { + f, err := os.Create(name) + if err != nil { + return err + } + _, err = f.WriteString(content) + if err != nil { + return err + } + err = f.Sync() + if err != nil { + return err + } + return f.Close() +} diff --git a/query_gen/spitter.go b/query_gen/spitter.go new file mode 100644 index 00000000..5159d1b8 --- /dev/null +++ b/query_gen/spitter.go @@ -0,0 +1,44 @@ +package main + +import "strings" +import "./lib" + +type PrimaryKeySpitter struct { + keys map[string]string +} + +func NewPrimaryKeySpitter() *PrimaryKeySpitter { + return &PrimaryKeySpitter{make(map[string]string)} +} + +func (spit *PrimaryKeySpitter) Hook(name string, args ...interface{}) error { + if name == "CreateTableStart" { + var found string + for _, key := range args[4].([]qgen.DB_Table_Key) { + if key.Type == "primary" { + expl := strings.Split(key.Columns, ",") + if len(expl) > 1 { + continue + } + found = key.Columns + } + if found != "" { + table := args[0].(string) + spit.keys[table] = found + } + } + } + return nil +} + +func (spit *PrimaryKeySpitter) Write() error { + out := `// Generated by Gosora's Query Generator. DO NOT EDIT. +package main + +var dbTablePrimaryKeys = map[string]string{ +` + for table, key := range spit.keys { + out += "\t\"" + table + "\":\"" + key + "\",\n" + } + return writeFile("./gen_tables.go", out+"}\n") +} diff --git a/query_gen/tables.go b/query_gen/tables.go new file mode 100644 index 00000000..634bfead --- /dev/null +++ b/query_gen/tables.go @@ -0,0 +1,363 @@ +/* WIP Under Construction */ +package main + +import "./lib" + +func createTables(adapter qgen.DB_Adapter) error { + qgen.Install.CreateTable("users", "utf8mb4", "utf8mb4_general_ci", + []qgen.DB_Table_Column{ + qgen.DB_Table_Column{"uid", "int", 0, false, true, ""}, + qgen.DB_Table_Column{"name", "varchar", 100, false, false, ""}, + qgen.DB_Table_Column{"password", "varchar", 100, false, false, ""}, + qgen.DB_Table_Column{"salt", "varchar", 80, false, false, "''"}, + qgen.DB_Table_Column{"group", "int", 0, false, false, ""}, + qgen.DB_Table_Column{"active", "boolean", 0, false, false, "0"}, + qgen.DB_Table_Column{"is_super_admin", "boolean", 0, false, false, "0"}, + qgen.DB_Table_Column{"createdAt", "createdAt", 0, false, false, ""}, + qgen.DB_Table_Column{"lastActiveAt", "datetime", 0, false, false, ""}, + qgen.DB_Table_Column{"session", "varchar", 200, false, false, "''"}, + qgen.DB_Table_Column{"last_ip", "varchar", 200, false, false, "0.0.0.0.0"}, + qgen.DB_Table_Column{"email", "varchar", 200, false, false, "''"}, + qgen.DB_Table_Column{"avatar", "varchar", 100, false, false, "''"}, + qgen.DB_Table_Column{"message", "text", 0, false, false, "''"}, + qgen.DB_Table_Column{"url_prefix", "varchar", 20, false, false, "''"}, + qgen.DB_Table_Column{"url_name", "varchar", 100, false, false, "''"}, + qgen.DB_Table_Column{"level", "smallint", 0, false, false, "0"}, + qgen.DB_Table_Column{"score", "int", 0, false, false, "0"}, + qgen.DB_Table_Column{"posts", "int", 0, false, false, "0"}, + qgen.DB_Table_Column{"bigposts", "int", 0, false, false, "0"}, + qgen.DB_Table_Column{"megaposts", "int", 0, false, false, "0"}, + qgen.DB_Table_Column{"topics", "int", 0, false, false, "0"}, + //qgen.DB_Table_Column{"penalty_count","int",0,false,false,"0"}, + qgen.DB_Table_Column{"temp_group", "int", 0, false, false, "0"}, // For temporary groups, set this to zero when a temporary group isn't in effect + }, + []qgen.DB_Table_Key{ + qgen.DB_Table_Key{"uid", "primary"}, + qgen.DB_Table_Key{"name", "unique"}, + }, + ) + + qgen.Install.CreateTable("users_groups", "utf8mb4", "utf8mb4_general_ci", + []qgen.DB_Table_Column{ + qgen.DB_Table_Column{"gid", "int", 0, false, true, ""}, + qgen.DB_Table_Column{"name", "varchar", 100, false, false, ""}, + qgen.DB_Table_Column{"permissions", "text", 0, false, false, ""}, + qgen.DB_Table_Column{"plugin_perms", "text", 0, false, false, ""}, + qgen.DB_Table_Column{"is_mod", "boolean", 0, false, false, "0"}, + qgen.DB_Table_Column{"is_admin", "boolean", 0, false, false, "0"}, + qgen.DB_Table_Column{"is_banned", "boolean", 0, false, false, "0"}, + qgen.DB_Table_Column{"tag", "varchar", 50, false, false, "''"}, + }, + []qgen.DB_Table_Key{ + qgen.DB_Table_Key{"gid", "primary"}, + }, + ) + + // What should we do about global penalties? Put them on the users table for speed? Or keep them here? + // Should we add IP Penalties? No, that's a stupid idea, just implement IP Bans properly. What about shadowbans? + // TODO: Perm overrides + // TODO: Add a mod-queue and other basic auto-mod features. This is needed for awaiting activation and the mod_queue penalty flag + // TODO: Add a penalty type where a user is stopped from creating plugin_socialgroups social groups + // TODO: Shadow bans. We will probably have a CanShadowBan permission for this, as we *really* don't want people using this lightly. + /*qgen.Install.CreateTable("users_penalties","","", + []qgen.DB_Table_Column{ + qgen.DB_Table_Column{"uid","int",0,false,false,""}, + qgen.DB_Table_Column{"element_id","int",0,false,false,""}, + qgen.DB_Table_Column{"element_type","varchar",50,false,false,""}, //forum, profile?, and social_group. Leave blank for global. + qgen.DB_Table_Column{"overrides","text",0,false,false,"{}"}, + + qgen.DB_Table_Column{"mod_queue","boolean",0,false,false,"0"}, + qgen.DB_Table_Column{"shadow_ban","boolean",0,false,false,"0"}, + qgen.DB_Table_Column{"no_avatar","boolean",0,false,false,"0"}, // Coming Soon. Should this be a perm override instead? + + // Do we *really* need rate-limit penalty types? Are we going to be allowing bots or something? + //qgen.DB_Table_Column{"posts_per_hour","int",0,false,false,"0"}, + //qgen.DB_Table_Column{"topics_per_hour","int",0,false,false,"0"}, + //qgen.DB_Table_Column{"posts_count","int",0,false,false,"0"}, + //qgen.DB_Table_Column{"topic_count","int",0,false,false,"0"}, + //qgen.DB_Table_Column{"last_hour","int",0,false,false,"0"}, // UNIX Time, as we don't need to do anything too fancy here. When an hour has elapsed since that time, reset the hourly penalty counters. + + qgen.DB_Table_Column{"issued_by","int",0,false,false,""}, + qgen.DB_Table_Column{"issued_at","createdAt",0,false,false,""}, + qgen.DB_Table_Column{"expires_at","datetime",0,false,false,""}, + }, + []qgen.DB_Table_Key{}, + )*/ + + qgen.Install.CreateTable("users_groups_scheduler", "", "", + []qgen.DB_Table_Column{ + qgen.DB_Table_Column{"uid", "int", 0, false, false, ""}, + qgen.DB_Table_Column{"set_group", "int", 0, false, false, ""}, + + qgen.DB_Table_Column{"issued_by", "int", 0, false, false, ""}, + qgen.DB_Table_Column{"issued_at", "createdAt", 0, false, false, ""}, + qgen.DB_Table_Column{"revert_at", "datetime", 0, false, false, ""}, + qgen.DB_Table_Column{"temporary", "boolean", 0, false, false, ""}, // special case for permanent bans to do the necessary bookkeeping, might be removed in the future + }, + []qgen.DB_Table_Key{ + qgen.DB_Table_Key{"uid", "primary"}, + }, + ) + + qgen.Install.CreateTable("emails", "", "", + []qgen.DB_Table_Column{ + qgen.DB_Table_Column{"email", "varchar", 200, false, false, ""}, + qgen.DB_Table_Column{"uid", "int", 0, false, false, ""}, + qgen.DB_Table_Column{"validated", "boolean", 0, false, false, "0"}, + qgen.DB_Table_Column{"token", "varchar", 200, false, false, "''"}, + }, + []qgen.DB_Table_Key{}, + ) + + qgen.Install.CreateTable("forums", "utf8mb4", "utf8mb4_general_ci", + []qgen.DB_Table_Column{ + qgen.DB_Table_Column{"fid", "int", 0, false, true, ""}, + qgen.DB_Table_Column{"name", "varchar", 100, false, false, ""}, + qgen.DB_Table_Column{"desc", "varchar", 200, false, false, ""}, + qgen.DB_Table_Column{"active", "boolean", 0, false, false, "1"}, + qgen.DB_Table_Column{"topicCount", "int", 0, false, false, "0"}, + qgen.DB_Table_Column{"preset", "varchar", 100, false, false, "''"}, + qgen.DB_Table_Column{"parentID", "int", 0, false, false, "0"}, + qgen.DB_Table_Column{"parentType", "varchar", 50, false, false, "''"}, + qgen.DB_Table_Column{"lastTopicID", "int", 0, false, false, "0"}, + qgen.DB_Table_Column{"lastReplyerID", "int", 0, false, false, "0"}, + }, + []qgen.DB_Table_Key{ + qgen.DB_Table_Key{"fid", "primary"}, + }, + ) + + qgen.Install.CreateTable("forums_permissions", "", "", + []qgen.DB_Table_Column{ + qgen.DB_Table_Column{"fid", "int", 0, false, false, ""}, + qgen.DB_Table_Column{"gid", "int", 0, false, false, ""}, + qgen.DB_Table_Column{"preset", "varchar", 100, false, false, "''"}, + qgen.DB_Table_Column{"permissions", "text", 0, false, false, ""}, + }, + []qgen.DB_Table_Key{ + // TODO: Test to see that the compound primary key works + qgen.DB_Table_Key{"fid,gid", "primary"}, + }, + ) + + qgen.Install.CreateTable("topics", "utf8mb4", "utf8mb4_general_ci", + []qgen.DB_Table_Column{ + qgen.DB_Table_Column{"tid", "int", 0, false, true, ""}, + qgen.DB_Table_Column{"title", "varchar", 100, false, false, ""}, + qgen.DB_Table_Column{"content", "text", 0, false, false, ""}, + qgen.DB_Table_Column{"parsed_content", "text", 0, false, false, ""}, + qgen.DB_Table_Column{"createdAt", "createdAt", 0, false, false, ""}, + qgen.DB_Table_Column{"lastReplyAt", "datetime", 0, false, false, ""}, + qgen.DB_Table_Column{"lastReplyBy", "int", 0, false, false, ""}, + qgen.DB_Table_Column{"createdBy", "int", 0, false, false, ""}, + qgen.DB_Table_Column{"is_closed", "boolean", 0, false, false, "0"}, + qgen.DB_Table_Column{"sticky", "boolean", 0, false, false, "0"}, + qgen.DB_Table_Column{"parentID", "int", 0, false, false, "2"}, + qgen.DB_Table_Column{"ipaddress", "varchar", 200, false, false, "0.0.0.0.0"}, + qgen.DB_Table_Column{"postCount", "int", 0, false, false, "1"}, + qgen.DB_Table_Column{"likeCount", "int", 0, false, false, "0"}, + qgen.DB_Table_Column{"words", "int", 0, false, false, "0"}, + qgen.DB_Table_Column{"css_class", "varchar", 100, false, false, "''"}, + qgen.DB_Table_Column{"data", "varchar", 200, false, false, "''"}, + }, + []qgen.DB_Table_Key{ + qgen.DB_Table_Key{"tid", "primary"}, + }, + ) + + qgen.Install.CreateTable("replies", "utf8mb4", "utf8mb4_general_ci", + []qgen.DB_Table_Column{ + qgen.DB_Table_Column{"rid", "int", 0, false, true, ""}, + qgen.DB_Table_Column{"tid", "int", 0, false, false, ""}, + qgen.DB_Table_Column{"content", "text", 0, false, false, ""}, + qgen.DB_Table_Column{"parsed_content", "text", 0, false, false, ""}, + qgen.DB_Table_Column{"createdAt", "createdAt", 0, false, false, ""}, + qgen.DB_Table_Column{"createdBy", "int", 0, false, false, ""}, + qgen.DB_Table_Column{"lastEdit", "int", 0, false, false, "0"}, + qgen.DB_Table_Column{"lastEditBy", "int", 0, false, false, "0"}, + qgen.DB_Table_Column{"lastUpdated", "datetime", 0, false, false, ""}, + qgen.DB_Table_Column{"ipaddress", "varchar", 200, false, false, "0.0.0.0.0"}, + qgen.DB_Table_Column{"likeCount", "int", 0, false, false, "0"}, + qgen.DB_Table_Column{"words", "int", 0, false, false, "1"}, // ? - replies has a default of 1 and topics has 0? why? + qgen.DB_Table_Column{"actionType", "varchar", 20, false, false, "''"}, + }, + []qgen.DB_Table_Key{ + qgen.DB_Table_Key{"rid", "primary"}, + }, + ) + + qgen.Install.CreateTable("attachments", "utf8mb4", "utf8mb4_general_ci", + []qgen.DB_Table_Column{ + qgen.DB_Table_Column{"attachID", "int", 0, false, true, ""}, + qgen.DB_Table_Column{"sectionID", "int", 0, false, false, "0"}, + qgen.DB_Table_Column{"sectionTable", "varchar", 200, false, false, "forums"}, + qgen.DB_Table_Column{"originID", "int", 0, false, false, ""}, + qgen.DB_Table_Column{"originTable", "varchar", 200, false, false, "replies"}, + qgen.DB_Table_Column{"uploadedBy", "int", 0, false, false, ""}, + qgen.DB_Table_Column{"path", "varchar", 200, false, false, ""}, + }, + []qgen.DB_Table_Key{ + qgen.DB_Table_Key{"attachID", "primary"}, + }, + ) + + qgen.Install.CreateTable("revisions", "utf8mb4", "utf8mb4_general_ci", + []qgen.DB_Table_Column{ + qgen.DB_Table_Column{"index", "int", 0, false, false, ""}, // TODO: Replace this with a proper revision ID x.x + qgen.DB_Table_Column{"content", "text", 0, false, false, ""}, + qgen.DB_Table_Column{"contentID", "int", 0, false, false, ""}, + qgen.DB_Table_Column{"contentType", "varchar", 100, false, false, "replies"}, + }, + []qgen.DB_Table_Key{}, + ) + + qgen.Install.CreateTable("users_replies", "utf8mb4", "utf8mb4_general_ci", + []qgen.DB_Table_Column{ + qgen.DB_Table_Column{"rid", "int", 0, false, true, ""}, + qgen.DB_Table_Column{"uid", "int", 0, false, false, ""}, + qgen.DB_Table_Column{"content", "text", 0, false, false, ""}, + qgen.DB_Table_Column{"parsed_content", "text", 0, false, false, ""}, + qgen.DB_Table_Column{"createdAt", "createdAt", 0, false, false, ""}, + qgen.DB_Table_Column{"createdBy", "int", 0, false, false, ""}, + qgen.DB_Table_Column{"lastEdit", "int", 0, false, false, ""}, + qgen.DB_Table_Column{"lastEditBy", "int", 0, false, false, ""}, + qgen.DB_Table_Column{"ipaddress", "varchar", 200, false, false, "0.0.0.0.0"}, + }, + []qgen.DB_Table_Key{ + qgen.DB_Table_Key{"rid", "primary"}, + }, + ) + + qgen.Install.CreateTable("likes", "", "", + []qgen.DB_Table_Column{ + qgen.DB_Table_Column{"weight", "tinyint", 0, false, false, "1"}, + qgen.DB_Table_Column{"targetItem", "int", 0, false, false, ""}, + qgen.DB_Table_Column{"targetType", "varchar", 50, false, false, "replies"}, + qgen.DB_Table_Column{"sentBy", "int", 0, false, false, ""}, + qgen.DB_Table_Column{"recalc", "tinyint", 0, false, false, "0"}, + }, + []qgen.DB_Table_Key{}, + ) + + qgen.Install.CreateTable("activity_stream_matches", "", "", + []qgen.DB_Table_Column{ + qgen.DB_Table_Column{"watcher", "int", 0, false, false, ""}, + qgen.DB_Table_Column{"asid", "int", 0, false, false, ""}, + }, + []qgen.DB_Table_Key{}, + ) + + qgen.Install.CreateTable("activity_stream", "", "", + []qgen.DB_Table_Column{ + qgen.DB_Table_Column{"asid", "int", 0, false, true, ""}, + qgen.DB_Table_Column{"actor", "int", 0, false, false, ""}, /* the one doing the act */ + qgen.DB_Table_Column{"targetUser", "int", 0, false, false, ""}, /* the user who created the item the actor is acting on, some items like forums may lack a targetUser field */ + qgen.DB_Table_Column{"event", "varchar", 50, false, false, ""}, /* mention, like, reply (as in the act of replying to an item, not the reply item type, you can "reply" to a forum by making a topic in it), friend_invite */ + qgen.DB_Table_Column{"elementType", "varchar", 50, false, false, ""}, /* topic, post (calling it post here to differentiate it from the 'reply' event), forum, user */ + qgen.DB_Table_Column{"elementID", "int", 0, false, false, ""}, /* the ID of the element being acted upon */ + }, + []qgen.DB_Table_Key{ + qgen.DB_Table_Key{"asid", "primary"}, + }, + ) + + qgen.Install.CreateTable("activity_subscriptions", "", "", + []qgen.DB_Table_Column{ + qgen.DB_Table_Column{"user", "int", 0, false, false, ""}, + qgen.DB_Table_Column{"targetID", "int", 0, false, false, ""}, /* the ID of the element being acted upon */ + qgen.DB_Table_Column{"targetType", "varchar", 50, false, false, ""}, /* topic, post (calling it post here to differentiate it from the 'reply' event), forum, user */ + qgen.DB_Table_Column{"level", "int", 0, false, false, "0"}, /* 0: Mentions (aka the global default for any post), 1: Replies To You, 2: All Replies*/ + }, + []qgen.DB_Table_Key{}, + ) + + /* Due to MySQL's design, we have to drop the unique keys for table settings, plugins, and themes down from 200 to 180 or it will error */ + qgen.Install.CreateTable("settings", "", "", + []qgen.DB_Table_Column{ + qgen.DB_Table_Column{"name", "varchar", 180, false, false, ""}, + qgen.DB_Table_Column{"content", "varchar", 250, false, false, ""}, + qgen.DB_Table_Column{"type", "varchar", 50, false, false, ""}, + qgen.DB_Table_Column{"constraints", "varchar", 200, false, false, "''"}, + }, + []qgen.DB_Table_Key{ + qgen.DB_Table_Key{"name", "unique"}, + }, + ) + + qgen.Install.CreateTable("word_filters", "", "", + []qgen.DB_Table_Column{ + qgen.DB_Table_Column{"wfid", "int", 0, false, true, ""}, + qgen.DB_Table_Column{"find", "varchar", 200, false, false, ""}, + qgen.DB_Table_Column{"replacement", "varchar", 200, false, false, ""}, + }, + []qgen.DB_Table_Key{ + qgen.DB_Table_Key{"wfid", "primary"}, + }, + ) + + qgen.Install.CreateTable("plugins", "", "", + []qgen.DB_Table_Column{ + qgen.DB_Table_Column{"uname", "varchar", 180, false, false, ""}, + qgen.DB_Table_Column{"active", "boolean", 0, false, false, "0"}, + qgen.DB_Table_Column{"installed", "boolean", 0, false, false, "0"}, + }, + []qgen.DB_Table_Key{ + qgen.DB_Table_Key{"uname", "unique"}, + }, + ) + + qgen.Install.CreateTable("themes", "", "", + []qgen.DB_Table_Column{ + qgen.DB_Table_Column{"uname", "varchar", 180, false, false, ""}, + qgen.DB_Table_Column{"default", "boolean", 0, false, false, "0"}, + }, + []qgen.DB_Table_Key{ + qgen.DB_Table_Key{"uname", "unique"}, + }, + ) + + qgen.Install.CreateTable("widgets", "", "", + []qgen.DB_Table_Column{ + qgen.DB_Table_Column{"position", "int", 0, false, false, ""}, + qgen.DB_Table_Column{"side", "varchar", 100, false, false, ""}, + qgen.DB_Table_Column{"type", "varchar", 100, false, false, ""}, + qgen.DB_Table_Column{"active", "boolean", 0, false, false, "0"}, + qgen.DB_Table_Column{"location", "varchar", 100, false, false, ""}, + qgen.DB_Table_Column{"data", "text", 0, false, false, "''"}, + }, + []qgen.DB_Table_Key{}, + ) + + qgen.Install.CreateTable("moderation_logs", "", "", + []qgen.DB_Table_Column{ + qgen.DB_Table_Column{"action", "varchar", 100, false, false, ""}, + qgen.DB_Table_Column{"elementID", "int", 0, false, false, ""}, + qgen.DB_Table_Column{"elementType", "varchar", 100, false, false, ""}, + qgen.DB_Table_Column{"ipaddress", "varchar", 200, false, false, ""}, + qgen.DB_Table_Column{"actorID", "int", 0, false, false, ""}, + qgen.DB_Table_Column{"doneAt", "datetime", 0, false, false, ""}, + }, + []qgen.DB_Table_Key{}, + ) + + qgen.Install.CreateTable("administration_logs", "", "", + []qgen.DB_Table_Column{ + qgen.DB_Table_Column{"action", "varchar", 100, false, false, ""}, + qgen.DB_Table_Column{"elementID", "int", 0, false, false, ""}, + qgen.DB_Table_Column{"elementType", "varchar", 100, false, false, ""}, + qgen.DB_Table_Column{"ipaddress", "varchar", 200, false, false, ""}, + qgen.DB_Table_Column{"actorID", "int", 0, false, false, ""}, + qgen.DB_Table_Column{"doneAt", "datetime", 0, false, false, ""}, + }, + []qgen.DB_Table_Key{}, + ) + + qgen.Install.CreateTable("sync", "", "", + []qgen.DB_Table_Column{ + qgen.DB_Table_Column{"last_update", "datetime", 0, false, false, ""}, + }, + []qgen.DB_Table_Key{}, + ) + + return nil +} diff --git a/routes.go b/routes.go index 2419724b..c7ce5790 100644 --- a/routes.go +++ b/routes.go @@ -433,11 +433,7 @@ func routeForums(w http.ResponseWriter, r *http.Request, user User) { //topic, user := forum.GetLast() //if topic.ID != 0 && user.ID != 0 { if forum.LastTopic.ID != 0 && forum.LastReplyer.ID != 0 { - forum.LastTopicTime, err = relativeTimeFromString(forum.LastTopic.LastReplyAt) - if err != nil { - InternalError(err, w) - return - } + forum.LastTopicTime = relativeTime(forum.LastTopic.LastReplyAt) } else { forum.LastTopicTime = "" } diff --git a/schema/mssql/query_activity_stream.sql b/schema/mssql/query_activity_stream.sql index b61301e4..bebd8d25 100644 --- a/schema/mssql/query_activity_stream.sql +++ b/schema/mssql/query_activity_stream.sql @@ -1,4 +1,3 @@ -DROP TABLE IF EXISTS [activity_stream]; CREATE TABLE [activity_stream] ( [asid] int not null IDENTITY, [actor] int not null, diff --git a/schema/mssql/query_activity_stream_matches.sql b/schema/mssql/query_activity_stream_matches.sql index 73821dc5..e6e9cda6 100644 --- a/schema/mssql/query_activity_stream_matches.sql +++ b/schema/mssql/query_activity_stream_matches.sql @@ -1,4 +1,3 @@ -DROP TABLE IF EXISTS [activity_stream_matches]; CREATE TABLE [activity_stream_matches] ( [watcher] int not null, [asid] int not null diff --git a/schema/mssql/query_activity_subscriptions.sql b/schema/mssql/query_activity_subscriptions.sql index 6c0a2df8..885b8f25 100644 --- a/schema/mssql/query_activity_subscriptions.sql +++ b/schema/mssql/query_activity_subscriptions.sql @@ -1,4 +1,3 @@ -DROP TABLE IF EXISTS [activity_subscriptions]; CREATE TABLE [activity_subscriptions] ( [user] int not null, [targetID] int not null, diff --git a/schema/mssql/query_administration_logs.sql b/schema/mssql/query_administration_logs.sql index 40c398e1..57aafd25 100644 --- a/schema/mssql/query_administration_logs.sql +++ b/schema/mssql/query_administration_logs.sql @@ -1,4 +1,3 @@ -DROP TABLE IF EXISTS [administration_logs]; CREATE TABLE [administration_logs] ( [action] nvarchar (100) not null, [elementID] int not null, diff --git a/schema/mssql/query_attachments.sql b/schema/mssql/query_attachments.sql index cadd3337..e70193fc 100644 --- a/schema/mssql/query_attachments.sql +++ b/schema/mssql/query_attachments.sql @@ -1,4 +1,3 @@ -DROP TABLE IF EXISTS [attachments]; CREATE TABLE [attachments] ( [attachID] int not null IDENTITY, [sectionID] int DEFAULT 0 not null, diff --git a/schema/mssql/query_emails.sql b/schema/mssql/query_emails.sql index a04ed884..decaeeaa 100644 --- a/schema/mssql/query_emails.sql +++ b/schema/mssql/query_emails.sql @@ -1,4 +1,3 @@ -DROP TABLE IF EXISTS [emails]; CREATE TABLE [emails] ( [email] nvarchar (200) not null, [uid] int not null, diff --git a/schema/mssql/query_forums.sql b/schema/mssql/query_forums.sql index 4d31b9de..0a5f9c33 100644 --- a/schema/mssql/query_forums.sql +++ b/schema/mssql/query_forums.sql @@ -1,4 +1,3 @@ -DROP TABLE IF EXISTS [forums]; CREATE TABLE [forums] ( [fid] int not null IDENTITY, [name] nvarchar (100) not null, diff --git a/schema/mssql/query_forums_permissions.sql b/schema/mssql/query_forums_permissions.sql index d0025818..0362ef56 100644 --- a/schema/mssql/query_forums_permissions.sql +++ b/schema/mssql/query_forums_permissions.sql @@ -1,4 +1,3 @@ -DROP TABLE IF EXISTS [forums_permissions]; CREATE TABLE [forums_permissions] ( [fid] int not null, [gid] int not null, diff --git a/schema/mssql/query_likes.sql b/schema/mssql/query_likes.sql index d56977b8..9e2733c6 100644 --- a/schema/mssql/query_likes.sql +++ b/schema/mssql/query_likes.sql @@ -1,4 +1,3 @@ -DROP TABLE IF EXISTS [likes]; CREATE TABLE [likes] ( [weight] tinyint DEFAULT 1 not null, [targetItem] int not null, diff --git a/schema/mssql/query_moderation_logs.sql b/schema/mssql/query_moderation_logs.sql index 4e64a148..3a235f33 100644 --- a/schema/mssql/query_moderation_logs.sql +++ b/schema/mssql/query_moderation_logs.sql @@ -1,4 +1,3 @@ -DROP TABLE IF EXISTS [moderation_logs]; CREATE TABLE [moderation_logs] ( [action] nvarchar (100) not null, [elementID] int not null, diff --git a/schema/mssql/query_plugins.sql b/schema/mssql/query_plugins.sql index 64c84660..b3eca739 100644 --- a/schema/mssql/query_plugins.sql +++ b/schema/mssql/query_plugins.sql @@ -1,4 +1,3 @@ -DROP TABLE IF EXISTS [plugins]; CREATE TABLE [plugins] ( [uname] nvarchar (180) not null, [active] bit DEFAULT 0 not null, diff --git a/schema/mssql/query_replies.sql b/schema/mssql/query_replies.sql index 745231de..be3f6c8d 100644 --- a/schema/mssql/query_replies.sql +++ b/schema/mssql/query_replies.sql @@ -1,4 +1,3 @@ -DROP TABLE IF EXISTS [replies]; CREATE TABLE [replies] ( [rid] int not null IDENTITY, [tid] int not null, @@ -6,8 +5,8 @@ CREATE TABLE [replies] ( [parsed_content] nvarchar (MAX) not null, [createdAt] datetime not null, [createdBy] int not null, - [lastEdit] int not null, - [lastEditBy] int not null, + [lastEdit] int DEFAULT 0 not null, + [lastEditBy] int DEFAULT 0 not null, [lastUpdated] datetime not null, [ipaddress] nvarchar (200) DEFAULT '0.0.0.0.0' not null, [likeCount] int DEFAULT 0 not null, diff --git a/schema/mssql/query_revisions.sql b/schema/mssql/query_revisions.sql index 66cb16de..2840faf1 100644 --- a/schema/mssql/query_revisions.sql +++ b/schema/mssql/query_revisions.sql @@ -1,4 +1,3 @@ -DROP TABLE IF EXISTS [revisions]; CREATE TABLE [revisions] ( [index] int not null, [content] nvarchar (MAX) not null, diff --git a/schema/mssql/query_settings.sql b/schema/mssql/query_settings.sql index fee55368..7db70181 100644 --- a/schema/mssql/query_settings.sql +++ b/schema/mssql/query_settings.sql @@ -1,4 +1,3 @@ -DROP TABLE IF EXISTS [settings]; CREATE TABLE [settings] ( [name] nvarchar (180) not null, [content] nvarchar (250) not null, diff --git a/schema/mssql/query_sync.sql b/schema/mssql/query_sync.sql index a540f503..41c357bd 100644 --- a/schema/mssql/query_sync.sql +++ b/schema/mssql/query_sync.sql @@ -1,4 +1,3 @@ -DROP TABLE IF EXISTS [sync]; CREATE TABLE [sync] ( [last_update] datetime not null ); \ No newline at end of file diff --git a/schema/mssql/query_themes.sql b/schema/mssql/query_themes.sql index 757095b0..42e23001 100644 --- a/schema/mssql/query_themes.sql +++ b/schema/mssql/query_themes.sql @@ -1,4 +1,3 @@ -DROP TABLE IF EXISTS [themes]; CREATE TABLE [themes] ( [uname] nvarchar (180) not null, [default] bit DEFAULT 0 not null, diff --git a/schema/mssql/query_topics.sql b/schema/mssql/query_topics.sql index cd42bfac..7113bf33 100644 --- a/schema/mssql/query_topics.sql +++ b/schema/mssql/query_topics.sql @@ -1,4 +1,3 @@ -DROP TABLE IF EXISTS [topics]; CREATE TABLE [topics] ( [tid] int not null IDENTITY, [title] nvarchar (100) not null, diff --git a/schema/mssql/query_users.sql b/schema/mssql/query_users.sql index fbfbfaf8..0c8b2578 100644 --- a/schema/mssql/query_users.sql +++ b/schema/mssql/query_users.sql @@ -1,4 +1,3 @@ -DROP TABLE IF EXISTS [users]; CREATE TABLE [users] ( [uid] int not null IDENTITY, [name] nvarchar (100) not null, diff --git a/schema/mssql/query_users_groups.sql b/schema/mssql/query_users_groups.sql index 867decda..2efc3563 100644 --- a/schema/mssql/query_users_groups.sql +++ b/schema/mssql/query_users_groups.sql @@ -1,4 +1,3 @@ -DROP TABLE IF EXISTS [users_groups]; CREATE TABLE [users_groups] ( [gid] int not null IDENTITY, [name] nvarchar (100) not null, diff --git a/schema/mssql/query_users_groups_scheduler.sql b/schema/mssql/query_users_groups_scheduler.sql index 7869365e..07a8e2d7 100644 --- a/schema/mssql/query_users_groups_scheduler.sql +++ b/schema/mssql/query_users_groups_scheduler.sql @@ -1,4 +1,3 @@ -DROP TABLE IF EXISTS [users_groups_scheduler]; CREATE TABLE [users_groups_scheduler] ( [uid] int not null, [set_group] int not null, diff --git a/schema/mssql/query_users_replies.sql b/schema/mssql/query_users_replies.sql index f4a24d91..ced6bea6 100644 --- a/schema/mssql/query_users_replies.sql +++ b/schema/mssql/query_users_replies.sql @@ -1,4 +1,3 @@ -DROP TABLE IF EXISTS [users_replies]; CREATE TABLE [users_replies] ( [rid] int not null IDENTITY, [uid] int not null, diff --git a/schema/mssql/query_widgets.sql b/schema/mssql/query_widgets.sql index 1cb52246..11d42d9c 100644 --- a/schema/mssql/query_widgets.sql +++ b/schema/mssql/query_widgets.sql @@ -1,4 +1,3 @@ -DROP TABLE IF EXISTS [widgets]; CREATE TABLE [widgets] ( [position] int not null, [side] nvarchar (100) not null, diff --git a/schema/mssql/query_word_filters.sql b/schema/mssql/query_word_filters.sql index 036c405a..c8817642 100644 --- a/schema/mssql/query_word_filters.sql +++ b/schema/mssql/query_word_filters.sql @@ -1,4 +1,3 @@ -DROP TABLE IF EXISTS [word_filters]; CREATE TABLE [word_filters] ( [wfid] int not null IDENTITY, [find] nvarchar (200) not null, diff --git a/schema/mysql/query_replies.sql b/schema/mysql/query_replies.sql index ce1860fe..83ece34b 100644 --- a/schema/mysql/query_replies.sql +++ b/schema/mysql/query_replies.sql @@ -5,8 +5,8 @@ CREATE TABLE `replies` ( `parsed_content` text not null, `createdAt` datetime not null, `createdBy` int not null, - `lastEdit` int not null, - `lastEditBy` int not null, + `lastEdit` int DEFAULT 0 not null, + `lastEditBy` int DEFAULT 0 not null, `lastUpdated` datetime not null, `ipaddress` varchar(200) DEFAULT '0.0.0.0.0' not null, `likeCount` int DEFAULT 0 not null, diff --git a/schema/pgsql/query_replies.sql b/schema/pgsql/query_replies.sql index 1718336d..cba5c316 100644 --- a/schema/pgsql/query_replies.sql +++ b/schema/pgsql/query_replies.sql @@ -5,8 +5,8 @@ CREATE TABLE `replies` ( `parsed_content` text not null, `createdAt` timestamp not null, `createdBy` int not null, - `lastEdit` int not null, - `lastEditBy` int not null, + `lastEdit` int DEFAULT 0 not null, + `lastEditBy` int DEFAULT 0 not null, `lastUpdated` timestamp not null, `ipaddress` varchar (200) DEFAULT '0.0.0.0.0' not null, `likeCount` int DEFAULT 0 not null, diff --git a/tasks.go b/tasks.go index 2175a407..36b83ef2 100644 --- a/tasks.go +++ b/tasks.go @@ -6,7 +6,10 @@ */ package main -import "time" +import ( + "log" + "time" +) var lastSync time.Time @@ -28,12 +31,14 @@ func handleExpiredScheduledGroups() error { if err != nil { return err } - _, err = replaceScheduleGroupStmt.Exec(uid, 0, 0, time.Now(), false) + _, err = replaceScheduleGroupStmt.Exec(uid, 0, 0, time.Now(), false, uid) if err != nil { + log.Print("Unable to replace the scheduled group") return err } _, err = setTempGroupStmt.Exec(0, uid) if err != nil { + log.Print("Unable to reset the tempgroup") return err } if ok { @@ -54,16 +59,19 @@ func handleServerSync() error { // TODO: A more granular sync err = fstore.LoadForums() if err != nil { + log.Print("Unable to reload the forums") return err } // TODO: Resync the groups // TODO: Resync the permissions err = LoadSettings() if err != nil { + log.Print("Unable to reload the settings") return err } err = LoadWordFilters() if err != nil { + log.Print("Unable to reload the word filters") return err } } diff --git a/template_init.go b/template_init.go index fef49b38..e39a7a46 100644 --- a/template_init.go +++ b/template_init.go @@ -110,7 +110,7 @@ func compileTemplates() error { log.Print("Compiling the templates") - topic := TopicUser{1, "blah", "Blah", "Hey there!", 0, false, false, "Date", "Date", 0, "", "127.0.0.1", 0, 1, "classname", "weird-data", buildProfileURL("fake-user", 62), "Fake User", config.DefaultGroup, "", 0, "", "", "", "", "", 58, false} + topic := TopicUser{1, "blah", "Blah", "Hey there!", 0, false, false, "Date", time.Now(), "Date", 0, "", "127.0.0.1", 0, 1, "classname", "weird-data", buildProfileURL("fake-user", 62), "Fake User", config.DefaultGroup, "", 0, "", "", "", "", "", 58, false} var replyList []ReplyUser replyList = append(replyList, ReplyUser{0, 0, "Yo!", "Yo!", 0, "alice", "Alice", config.DefaultGroup, "", 0, 0, "", "", 0, "", "", "", "", 0, "127.0.0.1", false, 1, "", ""}) diff --git a/topic.go b/topic.go index e3067b3f..65a3ef02 100644 --- a/topic.go +++ b/topic.go @@ -17,15 +17,16 @@ import ( // ? - Add a TopicMeta struct for *Forums? type Topic struct { - ID int - Link string - Title string - Content string - CreatedBy int - IsClosed bool - Sticky bool - CreatedAt string - LastReplyAt string + ID int + Link string + Title string + Content string + CreatedBy int + IsClosed bool + Sticky bool + CreatedAt string + LastReplyAt time.Time + RelativeLastReplyAt string //LastReplyBy int ParentID int Status string // Deprecated. Marked for removal. @@ -37,15 +38,16 @@ type Topic struct { } type TopicUser struct { - ID int - Link string - Title string - Content string - CreatedBy int - IsClosed bool - Sticky bool - CreatedAt string - LastReplyAt string + ID int + Link string + Title string + Content string + CreatedBy int + IsClosed bool + Sticky bool + CreatedAt string + LastReplyAt time.Time + RelativeLastReplyAt string //LastReplyBy int ParentID int Status string // Deprecated. Marked for removal. diff --git a/user.go b/user.go index d1cafca6..5eecce56 100644 --- a/user.go +++ b/user.go @@ -15,6 +15,9 @@ import ( "golang.org/x/crypto/bcrypt" ) +// TODO: Replace any literals with this +var banGroup = 4 + var guestUser = User{ID: 0, Link: "#", Group: 6, Perms: GuestPerms} //func(real_password string, password string, salt string) (err error) @@ -59,7 +62,7 @@ type Email struct { } func (user *User) Ban(duration time.Duration, issuedBy int) error { - return user.ScheduleGroupUpdate(4, issuedBy, duration) + return user.ScheduleGroupUpdate(banGroup, issuedBy, duration) } func (user *User) Unban() error { @@ -80,7 +83,7 @@ func (user *User) ScheduleGroupUpdate(gid int, issuedBy int, duration time.Durat } revertAt := time.Now().Add(duration) - _, err := replaceScheduleGroupStmt.Exec(user.ID, gid, issuedBy, revertAt, temporary) + _, err := replaceScheduleGroupStmt.Exec(user.ID, gid, issuedBy, revertAt, temporary, user.ID) if err != nil { return err } @@ -94,7 +97,7 @@ func (user *User) ScheduleGroupUpdate(gid int, issuedBy int, duration time.Durat // TODO: Use a transaction to avoid race conditions func (user *User) RevertGroupUpdate() error { - _, err := replaceScheduleGroupStmt.Exec(user.ID, 0, 0, time.Now(), false) + _, err := replaceScheduleGroupStmt.Exec(user.ID, 0, 0, time.Now(), false, user.ID) if err != nil { return err } @@ -121,6 +124,21 @@ func (user *User) Activate() (err error) { return err } +// TODO: Write tests for this +// TODO: Delete this user's content too? +// TODO: Expose this to the admin? +func (user *User) Delete() error { + _, err := deleteUserStmt.Exec(user.ID) + if err != nil { + return err + } + ucache, ok := users.(UserCache) + if ok { + ucache.CacheRemove(user.ID) + } + return err +} + func (user *User) ChangeName(username string) (err error) { _, err = setUsernameStmt.Exec(username, user.ID) ucache, ok := users.(UserCache) diff --git a/user_store.go b/user_store.go index 1f66b872..08514772 100644 --- a/user_store.go +++ b/user_store.go @@ -69,7 +69,7 @@ func NewMemoryUserStore(capacity int) *MemoryUserStore { // Add an admin version of register_stmt with more flexibility? // create_account_stmt, err = db.Prepare("INSERT INTO - registerStmt, err := qgen.Builder.SimpleInsert("users", "name, email, password, salt, group, is_super_admin, session, active, message", "?,?,?,?,?,0,'',?,''") + registerStmt, err := qgen.Builder.SimpleInsert("users", "name, email, password, salt, group, is_super_admin, session, active, message, createdAt, lastActiveAt", "?,?,?,?,?,0,'',?,'',UTC_TIMESTAMP(),UTC_TIMESTAMP()") if err != nil { log.Fatal(err) } @@ -289,7 +289,11 @@ func (mus *MemoryUserStore) Reload(id int) error { } func (mus *MemoryUserStore) Exists(id int) bool { - return mus.exists.QueryRow(id).Scan(&id) == nil + err := mus.exists.QueryRow(id).Scan(&id) + if err != nil && err != ErrNoRows { + LogError(err) + } + return err != ErrNoRows } func (mus *MemoryUserStore) CacheSet(item *User) error { @@ -315,8 +319,8 @@ func (mus *MemoryUserStore) CacheAdd(item *User) error { } mus.Lock() mus.items[item.ID] = item + mus.length = int64(len(mus.items)) mus.Unlock() - atomic.AddInt64(&mus.length, 1) return nil } @@ -325,12 +329,17 @@ func (mus *MemoryUserStore) CacheAddUnsafe(item *User) error { return ErrStoreCapacityOverflow } mus.items[item.ID] = item - atomic.AddInt64(&mus.length, 1) + mus.length = int64(len(mus.items)) return nil } func (mus *MemoryUserStore) CacheRemove(id int) error { mus.Lock() + _, ok := mus.items[id] + if !ok { + mus.Unlock() + return ErrNoRows + } delete(mus.items, id) mus.Unlock() atomic.AddInt64(&mus.length, -1) @@ -338,11 +347,16 @@ func (mus *MemoryUserStore) CacheRemove(id int) error { } func (mus *MemoryUserStore) CacheRemoveUnsafe(id int) error { + _, ok := mus.items[id] + if !ok { + return ErrNoRows + } delete(mus.items, id) atomic.AddInt64(&mus.length, -1) return nil } +// TODO: Change active to a bool? func (mus *MemoryUserStore) Create(username string, password string, email string, group int, active int) (int, error) { // Is this username already taken..? err := mus.usernameExists.QueryRow(username).Scan(&username) @@ -421,7 +435,7 @@ func NewSQLUserStore() *SQLUserStore { // Add an admin version of register_stmt with more flexibility? // create_account_stmt, err = db.Prepare("INSERT INTO - registerStmt, err := qgen.Builder.SimpleInsert("users", "name, email, password, salt, group, is_super_admin, session, active, message", "?,?,?,?,?,0,'',?,''") + registerStmt, err := qgen.Builder.SimpleInsert("users", "name, email, password, salt, group, is_super_admin, session, active, message, createdAt, lastActiveAt", "?,?,?,?,?,0,'',?,'',UTC_TIMESTAMP(),UTC_TIMESTAMP()") if err != nil { log.Fatal(err) } @@ -527,7 +541,11 @@ func (mus *SQLUserStore) BypassGet(id int) (*User, error) { } func (mus *SQLUserStore) Exists(id int) bool { - return mus.exists.QueryRow(id).Scan(&id) == nil + err := mus.exists.QueryRow(id).Scan(&id) + if err != nil && err != ErrNoRows { + LogError(err) + } + return err != ErrNoRows } func (mus *SQLUserStore) Create(username string, password string, email string, group int, active int) (int, error) {