Skip to content

Commit f02ec5d

Browse files
committed
test: restore SQLite statistics after plan tests
The two SQLite plan tests ran ANALYZE and then deleted every row in sqlite_stat1. A run against a SQLite database with its own statistics lost them, so later queries in that database could choose different plans. restoring_sqlite_statistics now saves each sqlite_stat table before the block, puts the saved rows back, reloads the statistics, and drops the statistics tables that the block created.
1 parent 8a54e28 commit f02ec5d

3 files changed

Lines changed: 55 additions & 33 deletions

File tree

‎test/database_test_helper.rb‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,30 @@ def suspend_sqlite_busy_wait(connection)
8383
connection.raw_connection.busy_handler_timeout = configured_sqlite_busy_handler_timeout
8484
end
8585

86+
def restoring_sqlite_statistics(connection)
87+
saved = sqlite_statistics_tables(connection).to_h { |table| [ table, connection.select_all("SELECT * FROM #{table}") ] }
88+
yield
89+
ensure
90+
restore_sqlite_statistics(connection, saved) if saved
91+
end
92+
93+
def restore_sqlite_statistics(connection, saved)
94+
database = connection.raw_connection
95+
saved.each do |table, statistics|
96+
placeholders = Array.new(statistics.columns.length, "?").join(", ")
97+
database.execute("DELETE FROM #{table}")
98+
statistics.rows.each do |row|
99+
database.execute("INSERT INTO #{table} (#{statistics.columns.join(", ")}) VALUES (#{placeholders})", row)
100+
end
101+
end
102+
database.execute("ANALYZE sqlite_schema")
103+
(sqlite_statistics_tables(connection) - saved.keys).each { |table| database.execute("DROP TABLE #{table}") }
104+
end
105+
106+
def sqlite_statistics_tables(connection)
107+
connection.select_values("SELECT name FROM sqlite_schema WHERE type = 'table' AND name LIKE 'sqlite\\_stat%' ESCAPE '\\'")
108+
end
109+
86110
def configured_sqlite_busy_handler_timeout
87111
SolidObjects::Record
88112
.connection_pool

‎test/integration/activation_candidates_test.rb‎

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,23 +38,22 @@ def run
3838
skip "requires a SQLite query plan" unless database_family == :sqlite
3939

4040
now = SolidObjects.database_adapter.database_now
41-
SolidObjects::Instance.insert_all!(Array.new(3_000) { |index|
42-
{ actor_type: "activation-candidates", actor_id: "idle-#{index}", state: {}, created_at: now, updated_at: now }
43-
})
4441
connection = SolidObjects::Record.connection
45-
connection.execute("ANALYZE")
46-
analyzed_tables = connection.select_values("SELECT DISTINCT tbl FROM sqlite_stat1")
42+
restoring_sqlite_statistics(connection) do
43+
SolidObjects::Instance.insert_all!(Array.new(3_000) { |index|
44+
{ actor_type: "activation-candidates", actor_id: "idle-#{index}", state: {}, created_at: now, updated_at: now }
45+
})
46+
connection.execute("ANALYZE")
47+
analyzed_tables = connection.select_values("SELECT DISTINCT tbl FROM sqlite_stat1")
4748

48-
assert_includes analyzed_tables, SolidObjects::Instance.table_name
49-
refute_includes analyzed_tables, SolidObjects::ClaimedMessage.table_name
49+
assert_includes analyzed_tables, SolidObjects::Instance.table_name
50+
refute_includes analyzed_tables, SolidObjects::ClaimedMessage.table_name
5051

51-
plan = sqlite_query_plan(connection, SolidObjects::ClaimedMessage.table_name) { claimed_instance_ids(now) }
52+
plan = sqlite_query_plan(connection, SolidObjects::ClaimedMessage.table_name) { claimed_instance_ids(now) }
5253

53-
assert_match(/\A(SCAN|SEARCH) #{SolidObjects::ClaimedMessage.table_name}\b/, plan.first, plan.join("\n"))
54-
assert plan.none? { |step| step.start_with?("SCAN #{SolidObjects::Instance.table_name}") }, plan.join("\n")
55-
ensure
56-
connection&.execute("DELETE FROM sqlite_stat1")
57-
connection&.execute("ANALYZE sqlite_schema")
54+
assert_match(/\A(SCAN|SEARCH) #{SolidObjects::ClaimedMessage.table_name}\b/, plan.first, plan.join("\n"))
55+
assert plan.none? { |step| step.start_with?("SCAN #{SolidObjects::Instance.table_name}") }, plan.join("\n")
56+
end
5857
end
5958

6059
private

‎test/integration/effect_recovery_candidates_test.rb‎

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -44,27 +44,26 @@ def run
4444

4545
now = Time.current
4646
effect_ids = Array.new(3_000) { SecureRandom.uuid }
47-
SolidObjects::Effect.insert_all!(effect_ids.map { |effect_id|
48-
{ message_id: @message.id, instance_id: @message.instance_id, effect_id:, name: "work", arguments: {},
49-
status: "completed", max_attempts: 3, available_at: now, completed_at: now, created_at: now, updated_at: now }
50-
})
51-
SolidObjects::EffectRecovery.insert_all!(effect_ids.last(300).map { |effect_id|
52-
{ effect_id:, instance_id: @message.instance_id, recovery_operation: "recover", status_operation: "status",
53-
created_at: now, updated_at: now }
54-
})
5547
connection = SolidObjects::Record.connection
56-
connection.execute("ANALYZE")
57-
poll_statistics = connection.select_value("SELECT stat FROM sqlite_stat1 WHERE idx = 'idx_so_effects_poll'")
58-
59-
assert_equal 3_000, poll_statistics.split[1].to_i
60-
61-
plan = connection.select_all("EXPLAIN QUERY PLAN #{recovery_candidates.to_sql}").map { |row| row["detail"] }
62-
63-
assert_match(/\ASEARCH #{SolidObjects::Effect.table_name} USING INDEX idx_so_effects_poll \(status=\?\)/, plan.first, plan.join("\n"))
64-
assert plan.none? { |step| step.start_with?("SCAN ") }, plan.join("\n")
65-
ensure
66-
connection&.execute("DELETE FROM sqlite_stat1")
67-
connection&.execute("ANALYZE sqlite_schema")
48+
restoring_sqlite_statistics(connection) do
49+
SolidObjects::Effect.insert_all!(effect_ids.map { |effect_id|
50+
{ message_id: @message.id, instance_id: @message.instance_id, effect_id:, name: "work", arguments: {},
51+
status: "completed", max_attempts: 3, available_at: now, completed_at: now, created_at: now, updated_at: now }
52+
})
53+
SolidObjects::EffectRecovery.insert_all!(effect_ids.last(300).map { |effect_id|
54+
{ effect_id:, instance_id: @message.instance_id, recovery_operation: "recover", status_operation: "status",
55+
created_at: now, updated_at: now }
56+
})
57+
connection.execute("ANALYZE")
58+
poll_statistics = connection.select_value("SELECT stat FROM sqlite_stat1 WHERE idx = 'idx_so_effects_poll'")
59+
60+
assert_equal 3_000, poll_statistics.split[1].to_i
61+
62+
plan = connection.select_all("EXPLAIN QUERY PLAN #{recovery_candidates.to_sql}").map { |row| row["detail"] }
63+
64+
assert_match(/\ASEARCH #{SolidObjects::Effect.table_name} USING INDEX idx_so_effects_poll \(status=\?\)/, plan.first, plan.join("\n"))
65+
assert plan.none? { |step| step.start_with?("SCAN ") }, plan.join("\n")
66+
end
6867
end
6968

7069
private

0 commit comments

Comments
 (0)