summaryrefslogtreecommitdiff
path: root/spec/support/database/prevent_cross_joins.rb
diff options
context:
space:
mode:
Diffstat (limited to 'spec/support/database/prevent_cross_joins.rb')
-rw-r--r--spec/support/database/prevent_cross_joins.rb77
1 files changed, 77 insertions, 0 deletions
diff --git a/spec/support/database/prevent_cross_joins.rb b/spec/support/database/prevent_cross_joins.rb
new file mode 100644
index 00000000000..789721ccd38
--- /dev/null
+++ b/spec/support/database/prevent_cross_joins.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+# This module tries to discover and prevent cross-joins across tables
+# This will forbid usage of tables between CI and main database
+# on a same query unless explicitly allowed by. This will change execution
+# from a given point to allow cross-joins. The state will be cleared
+# on a next test run.
+#
+# This method should be used to mark METHOD introducing cross-join
+# not a test using the cross-join.
+#
+# class User
+# def ci_owned_runners
+# ::Gitlab::Database.allow_cross_joins_across_databases!(url: link-to-issue-url)
+#
+# ...
+# end
+# end
+
+module Database
+ module PreventCrossJoins
+ CrossJoinAcrossUnsupportedTablesError = Class.new(StandardError)
+
+ def self.validate_cross_joins!(sql)
+ return if Thread.current[:allow_cross_joins_across_databases]
+
+ # PgQuery might fail in some cases due to limited nesting:
+ # https://github.com/pganalyze/pg_query/issues/209
+ tables = PgQuery.parse(sql).tables
+
+ unless only_ci_or_only_main?(tables)
+ raise CrossJoinAcrossUnsupportedTablesError,
+ "Unsupported cross-join across '#{tables.join(", ")}' discovered " \
+ "when executing query '#{sql}'"
+ end
+ end
+
+ # Returns true if a set includes only CI tables, or includes only non-CI tables
+ def self.only_ci_or_only_main?(tables)
+ tables.all? { |table| CiTables.include?(table) } ||
+ tables.none? { |table| CiTables.include?(table) }
+ end
+
+ module SpecHelpers
+ def with_cross_joins_prevented
+ subscriber = ActiveSupport::Notifications.subscribe('sql.active_record') do |event|
+ ::Database::PreventCrossJoins.validate_cross_joins!(event.payload[:sql])
+ end
+
+ Thread.current[:allow_cross_joins_across_databases] = false
+
+ yield
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
+ end
+ end
+
+ module GitlabDatabaseMixin
+ def allow_cross_joins_across_databases(url:)
+ Thread.current[:allow_cross_joins_across_databases] = true
+ super
+ end
+ end
+ end
+end
+
+Gitlab::Database.singleton_class.prepend(
+ Database::PreventCrossJoins::GitlabDatabaseMixin)
+
+RSpec.configure do |config|
+ config.include(::Database::PreventCrossJoins::SpecHelpers)
+
+ # TODO: remove `:prevent_cross_joins` to enable the check by default
+ config.around(:each, :prevent_cross_joins) do |example|
+ with_cross_joins_prevented { example.run }
+ end
+end