このコミットが含まれているのは:
@@ -340,7 +340,7 @@ class CountsCondition
|
||||
return @definition.alias_source(root) if root.start_with?('aliases_')
|
||||
return [nil, @definition.name_expression(root)]
|
||||
end
|
||||
return [nil, "(#{ @population.latest_sql })"] if root == 'latest'
|
||||
return [nil, "(#{ @population.current_latest_sql })"] if root == 'latest'
|
||||
if root == 'user' && parts.any?
|
||||
expression = parts.first == 'id' ? 'v.created_by_user_id' :
|
||||
'(SELECT actor.name FROM users actor WHERE actor.id = v.created_by_user_id)'
|
||||
|
||||
@@ -20,7 +20,7 @@ class CountsDefaults
|
||||
details = details.transform_values do |value|
|
||||
value.split(' ').flat_map do |token|
|
||||
if resource == 'tags' && ['name', 'parents.name'].include?(token)
|
||||
token == 'name' ? name : ["parents.#{ name }", "parents.#{ aliases }"]
|
||||
token == 'name' ? name : "parents.#{ name }"
|
||||
elsif resource == 'tags' && token == 'aliases'
|
||||
aliases
|
||||
elsif resource == 'tags' && token.start_with?('aliases.')
|
||||
|
||||
@@ -41,8 +41,7 @@ class CountsPopulation
|
||||
raise ArgumentError
|
||||
end
|
||||
|
||||
def latest_sql(row = 'v', cutoff = nil)
|
||||
cutoff ||= history ? at : timestamp(params['at']) || at
|
||||
def latest_at_sql(row, cutoff)
|
||||
keys = definition.keys.map { "newer.#{ _1 } = #{ row }.#{ _1 }" }.join(' AND ')
|
||||
bound = definition.model.connection.quote(cutoff)
|
||||
<<~SQL.squish
|
||||
@@ -52,6 +51,14 @@ class CountsPopulation
|
||||
SQL
|
||||
end
|
||||
|
||||
def current_latest_sql(row = 'v')
|
||||
keys = definition.keys.map { "newer.#{ _1 } = #{ row }.#{ _1 }" }.join(' AND ')
|
||||
<<~SQL.squish
|
||||
NOT EXISTS (SELECT 1 FROM #{ definition.table } newer
|
||||
WHERE #{ keys } AND newer.version_no > #{ row }.version_no)
|
||||
SQL
|
||||
end
|
||||
|
||||
def changed_sql(key)
|
||||
column = definition.column(key)
|
||||
"(previous.id IS NOT NULL AND NOT (v.#{ column } <=> previous.#{ column }))"
|
||||
@@ -125,7 +132,7 @@ class CountsPopulation
|
||||
raise ArgumentError if point > at
|
||||
|
||||
@scope = @scope.where("v.#{ definition.time_column } <= ?", point)
|
||||
.where(Arel.sql(latest_sql('v', point))).where.not('v.event_type' => 'discard')
|
||||
.where(Arel.sql(latest_at_sql('v', point))).where.not('v.event_type' => 'discard')
|
||||
return
|
||||
end
|
||||
first = timestamp(params['from'])
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Common entrypoint for six version resources; legacy endpoints keep their contracts.
|
||||
class CountsTableQuery
|
||||
SQL_HINTS = ['MAX_EXECUTION_TIME(10000)',
|
||||
"SET_VAR(optimizer_switch='semijoin=off')"].freeze
|
||||
MAX_COLUMNS = 100
|
||||
class TooManyColumns < ArgumentError; end
|
||||
|
||||
@@ -127,7 +129,7 @@ class CountsTableQuery
|
||||
numbered AS (SELECT row_groups.*,
|
||||
ROW_NUMBER() OVER (ORDER BY #{ order_rows.join(', ') }) AS position FROM row_groups),
|
||||
group_totals AS (SELECT COUNT(*) AS group_count FROM row_groups)
|
||||
SELECT /*+ MAX_EXECUTION_TIME(10000) */ cells.*,
|
||||
SELECT /*+ #{ SQL_HINTS.join(' ') } */ cells.*,
|
||||
page_rows.position AS counts_position, group_totals.group_count AS counts_group_count
|
||||
FROM group_totals LEFT JOIN numbered page_rows
|
||||
ON page_rows.position > #{ (page - 1) * limit }
|
||||
@@ -175,7 +177,7 @@ class CountsTableQuery
|
||||
|
||||
def details
|
||||
query = population.scope.select(Arel.sql(
|
||||
"(#{ population.latest_sql }) AS counts_latest"))
|
||||
"(#{ population.current_latest_sql }) AS counts_latest"))
|
||||
order = sort_sql { detail_expression(_1) }
|
||||
query = query.order(Arel.sql((order + ['v.id ASC']).join(', ')))
|
||||
.limit(limit).offset((page - 1) * limit)
|
||||
@@ -253,11 +255,11 @@ class CountsTableQuery
|
||||
|
||||
if HistoryAccess.admin? || mode == 'summary'
|
||||
total = population.scope.except(:select)
|
||||
.optimizer_hints('MAX_EXECUTION_TIME(10000)').count
|
||||
.optimizer_hints(*SQL_HINTS).count
|
||||
return @record_counts = { total:, visible: total, denied: 0 }
|
||||
end
|
||||
total, denied = unrestricted_population.scope.except(:select)
|
||||
.optimizer_hints('MAX_EXECUTION_TIME(10000)').pick(Arel.sql('COUNT(*)'),
|
||||
.optimizer_hints(*SQL_HINTS).pick(Arel.sql('COUNT(*)'),
|
||||
Arel.sql("COALESCE(SUM(CASE WHEN #{ HistoryAccess.sql(@definition.model, 'v') } " +
|
||||
'THEN 0 ELSE 1 END), 0)'))
|
||||
# Related/previous-version predicates may differ under public authority.
|
||||
@@ -267,7 +269,7 @@ class CountsTableQuery
|
||||
total.to_i - denied.to_i
|
||||
else
|
||||
population.scope.except(:select)
|
||||
.optimizer_hints('MAX_EXECUTION_TIME(10000)').count
|
||||
.optimizer_hints(*SQL_HINTS).count
|
||||
end
|
||||
@record_counts = { total: total.to_i, denied: denied.to_i, visible: }
|
||||
end
|
||||
@@ -309,7 +311,7 @@ class CountsTableQuery
|
||||
prefix = "WITH cells AS (#{ sql })"
|
||||
@connection.select_value(<<~SQL).to_i
|
||||
#{ prefix }
|
||||
SELECT /*+ MAX_EXECUTION_TIME(10000) */ COUNT(*) FROM (
|
||||
SELECT /*+ #{ SQL_HINTS.join(' ') } */ COUNT(*) FROM (
|
||||
SELECT #{ keys.join(', ') } FROM cells GROUP BY #{ keys.join(', ') }
|
||||
) output_groups
|
||||
SQL
|
||||
@@ -335,7 +337,7 @@ class CountsTableQuery
|
||||
end
|
||||
values = @connection.select_rows(<<~SQL)
|
||||
#{ prefix }
|
||||
SELECT /*+ MAX_EXECUTION_TIME(10000) */ DISTINCT #{ keys.join(', ') } FROM cells
|
||||
SELECT /*+ #{ SQL_HINTS.join(' ') } */ DISTINCT #{ keys.join(', ') } FROM cells
|
||||
ORDER BY #{ order } LIMIT #{ MAX_COLUMNS + 1 }
|
||||
SQL
|
||||
raise TooManyColumns if values.length > MAX_COLUMNS
|
||||
@@ -395,7 +397,7 @@ class CountsTableQuery
|
||||
end
|
||||
|
||||
def detail_expression(key)
|
||||
return "(#{ population.latest_sql })" if key == 'latest'
|
||||
return "(#{ population.current_latest_sql })" if key == 'latest'
|
||||
return @definition.name_expression(key) if @definition.resource == 'tags' &&
|
||||
@definition.name_column(key)
|
||||
aliases = { 'tags' => 'tags_json', 'parents' => @definition.resource == 'posts' ?
|
||||
|
||||
@@ -91,6 +91,34 @@ RSpec.describe 'Counts table API', type: :request do
|
||||
expect(versions(query(predicate.merge('conditions.0.value' => 'true')))).to eq([3])
|
||||
end
|
||||
|
||||
it 'distinguishes the snapshot cutoff from the current latest flag and condition' do
|
||||
past = { definition: 'latest', at: '2024-01-31T16:00:00Z' }
|
||||
result = query(past)
|
||||
expect(versions(result)).to eq([2])
|
||||
expect(result['versions'].first['latest']).to be(false)
|
||||
predicate = { conditions: '', 'conditions.0.field' => 'latest',
|
||||
'conditions.0.operator' => 'eq', 'conditions.0.value' => 'false' }
|
||||
expect(versions(query(past.merge(predicate)))).to eq([2])
|
||||
expect(versions(query(past.merge(predicate).merge('conditions.0.value' => 'true'))))
|
||||
.to eq([])
|
||||
unchanged = query(definition: 'latest', at: '2024-02-01T00:00:00Z')
|
||||
expect(versions(unchanged)).to eq([3])
|
||||
expect(unchanged['versions'].first['latest']).to be(true)
|
||||
expect(versions(query(sort: 'latest:desc resource_id:asc version_no:asc'))).to eq([3, 1, 2])
|
||||
end
|
||||
|
||||
it 'checks the current latest flag beyond the execution timestamp' do
|
||||
PostVersion.create!(post: record, version_no: 4, event_type: 'update',
|
||||
url: record.url, title: 'after execution', tags: '', tags_json: [],
|
||||
parent_post_ids: '', created_at: Time.utc(2025, 2, 1))
|
||||
result = query
|
||||
expect(versions(result)).to eq([1, 2, 3])
|
||||
expect(result['versions'].map { _1['latest'] }).to eq([false, false, false])
|
||||
snapshot = query(definition: 'latest')
|
||||
expect(versions(snapshot)).to eq([3])
|
||||
expect(snapshot['versions'].first['latest']).to be(false)
|
||||
end
|
||||
|
||||
it 'keeps ANY, ALL and NONE distinct without multiplying parent records' do
|
||||
values = { conditions: '', 'conditions.0.field' => 'tags',
|
||||
'conditions.0.values' => '1', condition_values: '',
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする