|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +require 'sequel' |
| 4 | + |
| 5 | +require_relative 'sql_spec_helper/display' |
| 6 | +require_relative 'sql_spec_helper/daff_wrapper' |
| 7 | +require_relative 'sql_spec_helper/compare' |
| 8 | + |
| 9 | +class SqlSpecHelper |
| 10 | + attr_reader :sql, :db |
| 11 | + |
| 12 | + def initialize(solution_path) |
| 13 | + @sql = File.read(solution_path) |
| 14 | + @commands = sql_commands(@sql) |
| 15 | + @db = connect |
| 16 | + end |
| 17 | + |
| 18 | + # `show_daff_table: true` display diff table |
| 19 | + # `daff_csv_show_index: true` include index in test output |
| 20 | + def compare_with(expected, limit: 100, collapsed: false, show_daff_table: true, daff_csv_show_index: false, &block) |
| 21 | + sql_compare = SqlCompare.new( |
| 22 | + self, |
| 23 | + expected, |
| 24 | + cmds: @commands, |
| 25 | + limit: limit, |
| 26 | + collapsed: collapsed, |
| 27 | + show_daff_table: show_daff_table, |
| 28 | + daff_csv_show_index: daff_csv_show_index |
| 29 | + ) |
| 30 | + sql_compare.instance_eval(&block) if block |
| 31 | + |
| 32 | + sql_compare.spec |
| 33 | + sql_compare.actual |
| 34 | + end |
| 35 | + |
| 36 | + # The main method used when running user's code. |
| 37 | + # Returns an Array of `Sequel::Adapter::Dataset` unless commands contained only one `SELECT`. |
| 38 | + # Returns `nil` if no `SELECT`. |
| 39 | + def run_sql(cmds: nil, limit: 100, print: true, label: 'SELECT Results', collapsed: false, &block) |
| 40 | + Display.status("Running sql commands...") |
| 41 | + cmds ||= @commands |
| 42 | + results = Array(cmds).each_with_object([]) do |cmd, results| |
| 43 | + dataset = run_cmd(cmd) || [] |
| 44 | + result = dataset.to_a |
| 45 | + next if result.empty? |
| 46 | + |
| 47 | + lbl = label |
| 48 | + lbl += " (Top #{limit} of #{result.size})" if result.size > limit |
| 49 | + lbl = "-" + lbl if collapsed |
| 50 | + |
| 51 | + block.call(dataset, lbl) if block |
| 52 | + |
| 53 | + Display.table(result.take(limit), label: lbl, allow_preview: true) if print |
| 54 | + results.push(dataset) |
| 55 | + end |
| 56 | + |
| 57 | + if results.length > 1 |
| 58 | + results |
| 59 | + else |
| 60 | + results.first |
| 61 | + end |
| 62 | + |
| 63 | + rescue Sequel::DatabaseError => ex |
| 64 | + Display.print("ERROR", ex.message.strip) |
| 65 | + end |
| 66 | + |
| 67 | + private |
| 68 | + |
| 69 | + # Connect the database |
| 70 | + def connect |
| 71 | + Display.status "Connecting to database..." |
| 72 | + case ENV['DATABASE_TYPE'] |
| 73 | + when 'sqlite' |
| 74 | + Sequel.sqlite |
| 75 | + when 'postgres' |
| 76 | + Sequel.connect( |
| 77 | + adapter: 'postgres', |
| 78 | + host: ENV['PGHOST'], |
| 79 | + user: ENV['PGUSER'], |
| 80 | + port: ENV['PGPORT'], |
| 81 | + database: ENV['DATABASE_NAME'] || ENV['PGDATABASE'], |
| 82 | + ) |
| 83 | + when 'mssql' |
| 84 | + Sequel.connect( |
| 85 | + adapter: 'tinytds', |
| 86 | + host: ENV['MSSQL_HOST'], |
| 87 | + port: ENV['MSSQL_PORT'], |
| 88 | + user: ENV['MSSQL_USER'], |
| 89 | + password: ENV['MSSQL_PASS'], |
| 90 | + ) |
| 91 | + else |
| 92 | + raise "Unknown database type #{ENV['DATABASE_TYPE']}" |
| 93 | + end |
| 94 | + end |
| 95 | + |
| 96 | + def sql_commands(sql) |
| 97 | + split_sql_commands(clean_sql(sql)) |
| 98 | + end |
| 99 | + |
| 100 | + def run_cmd(cmd) |
| 101 | + select_cmd?(cmd) ? @db[cmd] : @db.run(cmd) |
| 102 | + end |
| 103 | + |
| 104 | + def clean_sql(sql) |
| 105 | + sql.gsub(/(\/\*([\s\S]*?)\*\/|--.*)/, "") |
| 106 | + end |
| 107 | + |
| 108 | + def select_cmd?(cmd) |
| 109 | + (cmd.strip =~ /^(SELECT|WITH)/i) == 0 |
| 110 | + end |
| 111 | + |
| 112 | + def split_sql_commands(sql) |
| 113 | + # first we want to seperate select statements into chunks |
| 114 | + chunks = sql.split(/;[ \n\r]*$/i).select {|s| !s.empty?}.chunk {|s| select_cmd?(s)} |
| 115 | + # select statements need to stay individual so that we can return individual datasets, but we can group other statements together |
| 116 | + chunks.each_with_object([]) do |(select, cmds), final| |
| 117 | + if select |
| 118 | + final.concat(cmds) |
| 119 | + else |
| 120 | + final.push(cmds.join(";\n")) |
| 121 | + end |
| 122 | + end |
| 123 | + end |
| 124 | +end |
0 commit comments