Module: TeakUtil::TestDatabaseName

Defined in:
lib/teak_util/test_database_name.rb

Overview

Derives a worktree-scoped test database name from a checkout's directory basename, and purges databases left behind by worktrees that no longer exist. Deliberately standalone-requirable (no dependency on the rest of teak_util) since it's typically evaluated from an ERB database config at app boot.

Constant Summary collapse

MAX_IDENTIFIER_LENGTH =

MySQL's identifier limit

64
HASH_LENGTH =
8
DEFAULT_WORKTREE_LISTER =
lambda do
  output = `git worktree list --porcelain 2>/dev/null`
  raise '`git worktree list --porcelain` failed. Are you in a git repository?' unless $CHILD_STATUS.success?

  output
end

Class Method Summary collapse

Class Method Details

.for_worktree(basename, base:, main_worktree:) ⇒ Object

Parameters:

  • basename (String)

    the checkout's directory basename

  • base (String)

    the database name used by the main (non-worktree) checkout, e.g. 'taro_posts_test'

  • main_worktree (String)

    the directory name of the main checkout, e.g. 'taro' — compared case-insensitively so main_worktree gets base back unmodified

Raises:

  • (ArgumentError)


30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/teak_util/test_database_name.rb', line 30

def for_worktree(basename, base:, main_worktree:)
  downcased = basename.downcase
  return base if downcased == main_worktree.downcase

  # core_app's `CREATE DATABASE #{db}` is unquoted, so a non-identifier character
  # is a syntax error, not a style choice.
  sanitized = downcased.gsub(/[^a-z0-9]+/, '_')
  budget = MAX_IDENTIFIER_LENGTH - base.length - 1 # 1 for the joining underscore
  raise ArgumentError, "base #{base.inspect} leaves no room for a worktree suffix" if budget <= 0

  if sanitized.length > budget
    hash = Digest::SHA256.hexdigest(sanitized)[0, HASH_LENGTH]
    truncated_length = budget - hash.length - 1 # 1 for the underscore before the hash
    raise ArgumentError, "base #{base.inspect} leaves no room for a worktree suffix" if truncated_length <= 0

    sanitized = "#{sanitized[0, truncated_length]}_#{hash}"
  end

  "#{base}_#{sanitized}"
end

.purge(connection:, base:, main_worktree:, input: $stdin, output: $stdout, worktree_lister: DEFAULT_WORKTREE_LISTER) ⇒ Object

Lists and drops test databases for worktrees that no longer exist.

Parameters:

  • connection (#fetch, #run)

    a Sequel-connection-shaped object

  • base (String)

    as for #for_worktree

  • main_worktree (String)

    as for #for_worktree

  • input (#gets) (defaults to: $stdin)

    prompt source

  • output (#puts, #print) (defaults to: $stdout)

    prompt/progress sink

  • worktree_lister (#call) (defaults to: DEFAULT_WORKTREE_LISTER)

    returns git worktree list --porcelain output; must raise on failure — the default does. A custom lister inherits that contract; a lister that swallows a failed listing will read as "no active worktrees" and offer to drop every database this basename owns.



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/teak_util/test_database_name.rb', line 62

def purge(connection:, base:, main_worktree:, input: $stdin, output: $stdout,
          worktree_lister: DEFAULT_WORKTREE_LISTER)
  escaped_base = base.gsub(/[\\_%]/) { |match| "\\#{match}" }
  rows = connection.fetch("SHOW DATABASES LIKE '#{escaped_base}\\_%'").map { |row| row.values.first }

  active = worktree_lister.call.lines
             .select { |line| line.start_with?('worktree ') }
             .map { |line| File.basename(line.sub('worktree ', '').strip) }
             .to_set { |basename| for_worktree(basename, base: base, main_worktree: main_worktree) }

  defunct = rows - active.to_a

  if defunct.empty?
    output.puts 'No defunct worktree test databases found.'
    return
  end

  output.puts "Found #{defunct.size} defunct worktree test database(s):"
  defunct.each { |db| output.puts "  - #{db}" }

  output.print "\nDrop all of them? [y/N] "
  answer = input.gets&.strip
  unless answer&.match?(/\Ay(es)?\z/i)
    output.puts 'Aborted.'
    return
  end

  defunct.each do |db|
    output.puts "Dropping #{db}..."
    connection.run("DROP DATABASE `#{db}`")
  end
  output.puts 'Done.'
end