このコミットが含まれているのは:
2026-11-01 04:22:26 +09:00
コミット 29002d5393
20個のファイルの変更、419行の追加、102行の削除
+1 -1
ファイルの表示
@@ -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)'
+1 -1
ファイルの表示
@@ -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.')
+10 -3
ファイルの表示
@@ -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'])
+10 -8
ファイルの表示
@@ -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' ?
+28
ファイルの表示
@@ -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: '',