DEV: remove exec_sql and replace with mini_sql

Introduce new patterns for direct sql that are safe and fast.

MiniSql is not prone to memory bloat that can happen with direct PG usage.
It also has an extremely fast materializer and very a convenient API

- DB.exec(sql, *params) => runs sql returns row count
- DB.query(sql, *params) => runs sql returns usable objects (not a hash)
- DB.query_hash(sql, *params) => runs sql returns an array of hashes
- DB.query_single(sql, *params) => runs sql and returns a flat one dimensional array
- DB.build(sql) => returns a sql builder

See more at: https://github.com/discourse/mini_sql
This commit is contained in:
Sam
2018-06-19 16:13:14 +10:00
parent cc3fc87dd7
commit 5f64fd0a21
112 changed files with 782 additions and 763 deletions

View File

@ -352,7 +352,7 @@ class Topic < ActiveRecord::Base
if !new_record? && !Discourse.readonly_mode?
# make sure data is set in table, this also allows us to change algorithm
# by simply nulling this column
exec_sql("UPDATE topics SET fancy_title = :fancy_title where id = :id", id: self.id, fancy_title: fancy_title)
DB.exec("UPDATE topics SET fancy_title = :fancy_title where id = :id", id: self.id, fancy_title: fancy_title)
end
end
@ -522,130 +522,140 @@ class Topic < ActiveRecord::Base
# Atomically creates the next post number
def self.next_post_number(topic_id, reply = false, whisper = false)
highest = exec_sql("SELECT coalesce(max(post_number),0) AS max FROM posts WHERE topic_id = ?", topic_id).first['max'].to_i
highest = DB.query_single("SELECT coalesce(max(post_number),0) AS max FROM posts WHERE topic_id = ?", topic_id).first.to_i
if whisper
result = exec_sql("UPDATE topics
SET highest_staff_post_number = ? + 1
WHERE id = ?
RETURNING highest_staff_post_number", highest, topic_id)
result = DB.query_single(<<~SQL, highest, topic_id)
UPDATE topics
SET highest_staff_post_number = ? + 1
WHERE id = ?
RETURNING highest_staff_post_number
SQL
result.first['highest_staff_post_number'].to_i
result.first.to_i
else
reply_sql = reply ? ", reply_count = reply_count + 1" : ""
result = exec_sql("UPDATE topics
SET highest_staff_post_number = :highest + 1,
highest_post_number = :highest + 1#{reply_sql},
posts_count = posts_count + 1
WHERE id = :topic_id
RETURNING highest_post_number", highest: highest, topic_id: topic_id)
result = DB.query_single(<<~SQL, highest: highest, topic_id: topic_id)
UPDATE topics
SET highest_staff_post_number = :highest + 1,
highest_post_number = :highest + 1#{reply_sql},
posts_count = posts_count + 1
WHERE id = :topic_id
RETURNING highest_post_number
SQL
result.first['highest_post_number'].to_i
result.first.to_i
end
end
def self.reset_all_highest!
exec_sql <<SQL
WITH
X as (
SELECT topic_id,
COALESCE(MAX(post_number), 0) highest_post_number
FROM posts
WHERE deleted_at IS NULL
GROUP BY topic_id
),
Y as (
SELECT topic_id,
coalesce(MAX(post_number), 0) highest_post_number,
count(*) posts_count,
max(created_at) last_posted_at
FROM posts
WHERE deleted_at IS NULL AND post_type <> 4
GROUP BY topic_id
)
UPDATE topics
SET
highest_staff_post_number = X.highest_post_number,
highest_post_number = Y.highest_post_number,
last_posted_at = Y.last_posted_at,
posts_count = Y.posts_count
FROM X, Y
WHERE
X.topic_id = topics.id AND
Y.topic_id = topics.id AND (
topics.highest_staff_post_number <> X.highest_post_number OR
topics.highest_post_number <> Y.highest_post_number OR
topics.last_posted_at <> Y.last_posted_at OR
topics.posts_count <> Y.posts_count
)
SQL
DB.exec <<~SQL
WITH
X as (
SELECT topic_id,
COALESCE(MAX(post_number), 0) highest_post_number
FROM posts
WHERE deleted_at IS NULL
GROUP BY topic_id
),
Y as (
SELECT topic_id,
coalesce(MAX(post_number), 0) highest_post_number,
count(*) posts_count,
max(created_at) last_posted_at
FROM posts
WHERE deleted_at IS NULL AND post_type <> 4
GROUP BY topic_id
)
UPDATE topics
SET
highest_staff_post_number = X.highest_post_number,
highest_post_number = Y.highest_post_number,
last_posted_at = Y.last_posted_at,
posts_count = Y.posts_count
FROM X, Y
WHERE
X.topic_id = topics.id AND
Y.topic_id = topics.id AND (
topics.highest_staff_post_number <> X.highest_post_number OR
topics.highest_post_number <> Y.highest_post_number OR
topics.last_posted_at <> Y.last_posted_at OR
topics.posts_count <> Y.posts_count
)
SQL
end
# If a post is deleted we have to update our highest post counters
def self.reset_highest(topic_id)
result = exec_sql "UPDATE topics
SET
highest_staff_post_number = (
SELECT COALESCE(MAX(post_number), 0) FROM posts
WHERE topic_id = :topic_id AND
deleted_at IS NULL
),
highest_post_number = (
SELECT COALESCE(MAX(post_number), 0) FROM posts
WHERE topic_id = :topic_id AND
deleted_at IS NULL AND
post_type <> 4
),
posts_count = (
SELECT count(*) FROM posts
WHERE deleted_at IS NULL AND
topic_id = :topic_id AND
post_type <> 4
),
result = DB.query_single(<<~SQL, topic_id: topic_id)
UPDATE topics
SET
highest_staff_post_number = (
SELECT COALESCE(MAX(post_number), 0) FROM posts
WHERE topic_id = :topic_id AND
deleted_at IS NULL
),
highest_post_number = (
SELECT COALESCE(MAX(post_number), 0) FROM posts
WHERE topic_id = :topic_id AND
deleted_at IS NULL AND
post_type <> 4
),
posts_count = (
SELECT count(*) FROM posts
WHERE deleted_at IS NULL AND
topic_id = :topic_id AND
post_type <> 4
),
last_posted_at = (
SELECT MAX(created_at) FROM posts
WHERE topic_id = :topic_id AND
deleted_at IS NULL AND
post_type <> 4
)
WHERE id = :topic_id
RETURNING highest_post_number", topic_id: topic_id
last_posted_at = (
SELECT MAX(created_at) FROM posts
WHERE topic_id = :topic_id AND
deleted_at IS NULL AND
post_type <> 4
)
WHERE id = :topic_id
RETURNING highest_post_number
SQL
highest_post_number = result.first['highest_post_number'].to_i
highest_post_number = result.first.to_i
# Update the forum topic user records
exec_sql "UPDATE topic_users
SET last_read_post_number = CASE
WHEN last_read_post_number > :highest THEN :highest
ELSE last_read_post_number
END,
highest_seen_post_number = CASE
WHEN highest_seen_post_number > :highest THEN :highest
ELSE highest_seen_post_number
END
WHERE topic_id = :topic_id",
highest: highest_post_number,
topic_id: topic_id
DB.exec(<<~SQL, highest: highest_post_number, topic_id: topic_id)
UPDATE topic_users
SET last_read_post_number = CASE
WHEN last_read_post_number > :highest THEN :highest
ELSE last_read_post_number
END,
highest_seen_post_number = CASE
WHEN highest_seen_post_number > :highest THEN :highest
ELSE highest_seen_post_number
END
WHERE topic_id = :topic_id
SQL
end
# This calculates the geometric mean of the posts and stores it with the topic
def self.calculate_avg_time(min_topic_age = nil)
builder = SqlBuilder.new("UPDATE topics
SET avg_time = x.gmean
FROM (SELECT topic_id,
round(exp(avg(ln(avg_time)))) AS gmean
FROM posts
WHERE avg_time > 0 AND avg_time IS NOT NULL
GROUP BY topic_id) AS x
/*where*/")
builder = DB.build <<~SQL
UPDATE topics
SET avg_time = x.gmean
FROM (SELECT topic_id,
round(exp(avg(ln(avg_time)))) AS gmean
FROM posts
WHERE avg_time > 0 AND avg_time IS NOT NULL
GROUP BY topic_id) AS x
/*where*/
SQL
builder.where("x.topic_id = topics.id AND
(topics.avg_time <> x.gmean OR topics.avg_time IS NULL)")
builder.where <<~SQL
x.topic_id = topics.id AND
(topics.avg_time <> x.gmean OR topics.avg_time IS NULL)
SQL
if min_topic_age
builder.where("topics.bumped_at > :bumped_at", bumped_at: min_topic_age)
@ -1179,30 +1189,30 @@ SQL
# OR if you have it archived as a user explicitly
sql = <<~SQL
SELECT 1
WHERE
(
SELECT count(*) FROM topic_allowed_groups tg
JOIN group_archived_messages gm
ON gm.topic_id = tg.topic_id AND
gm.group_id = tg.group_id
WHERE tg.group_id IN (SELECT g.group_id FROM group_users g WHERE g.user_id = :user_id)
AND tg.topic_id = :topic_id
) =
(
SELECT case when count(*) = 0 then -1 else count(*) end FROM topic_allowed_groups tg
WHERE tg.group_id IN (SELECT g.group_id FROM group_users g WHERE g.user_id = :user_id)
AND tg.topic_id = :topic_id
)
SELECT 1
WHERE
(
SELECT count(*) FROM topic_allowed_groups tg
JOIN group_archived_messages gm
ON gm.topic_id = tg.topic_id AND
gm.group_id = tg.group_id
WHERE tg.group_id IN (SELECT g.group_id FROM group_users g WHERE g.user_id = :user_id)
AND tg.topic_id = :topic_id
) =
(
SELECT case when count(*) = 0 then -1 else count(*) end FROM topic_allowed_groups tg
WHERE tg.group_id IN (SELECT g.group_id FROM group_users g WHERE g.user_id = :user_id)
AND tg.topic_id = :topic_id
)
UNION ALL
UNION ALL
SELECT 1 FROM topic_allowed_users tu
JOIN user_archived_messages um ON um.user_id = tu.user_id AND um.topic_id = tu.topic_id
WHERE tu.user_id = :user_id AND tu.topic_id = :topic_id
SQL
SELECT 1 FROM topic_allowed_users tu
JOIN user_archived_messages um ON um.user_id = tu.user_id AND um.topic_id = tu.topic_id
WHERE tu.user_id = :user_id AND tu.topic_id = :topic_id
SQL
User.exec_sql(sql, user_id: user.id, topic_id: id).to_a.length > 0
DB.exec(sql, user_id: user.id, topic_id: id) > 0
end
TIME_TO_FIRST_RESPONSE_SQL ||= <<-SQL
@ -1325,8 +1335,8 @@ SQL
) = 1
SQL
result = Topic.exec_sql(sql, private_message: Archetype.private_message, topic_id: self.id)
result.ntuples != 0
result = DB.exec(sql, private_message: Archetype.private_message, topic_id: self.id)
result != 0
end
def featured_link_root_domain