Skip to content

Commit 7ec245c

Browse files
Add saved query management functions. (#191)
* Add saved query management functions. * Fix linter errors Linter didn't like: ``` > src/cb/saved_query.cr:95:29 > [W] Lint/NotNil: Avoid using `not_nil!` > > name: @name.not_nil!, ``` So removed the `not_nil!`. It is now consistent with other functions in the existing code base by not type-annotating client method parameters * use single codegen thread for specs * Add error handling suggestsion from Adam's review
1 parent 8577c5c commit 7ec245c

6 files changed

Lines changed: 399 additions & 0 deletions

File tree

spec/cb/saved_query_spec.cr

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
require "../spec_helper"
2+
3+
Spectator.describe CB::SavedQueryList do
4+
subject(action) { described_class.new client: client, output: IO::Memory.new }
5+
6+
mock_client
7+
8+
let(saved_queries) { [Factory.saved_query, Factory.saved_query(id: "sqpvoqooxzdrriu6w3bhqo55c4", name: "Other Query")] }
9+
10+
describe "#validate" do
11+
it "validates that required arguments are present" do
12+
expect_missing_arg_error
13+
action.cluster_id = "pkdpq6yynjgjbps4otxd7il2u4"
14+
expect(&.validate).to be_true
15+
end
16+
end
17+
18+
describe "#run" do
19+
before_each do
20+
action.cluster_id = "pkdpq6yynjgjbps4otxd7il2u4"
21+
end
22+
23+
it "displays empty message when no queries" do
24+
expect(client).to receive(:get_saved_queries).and_return([] of CB::Model::SavedQuery)
25+
action.call
26+
expect(&.output.to_s).to eq "no saved queries\n"
27+
end
28+
29+
it "outputs table format" do
30+
expect(client).to receive(:get_saved_queries).and_return(saved_queries)
31+
action.call
32+
expect(&.output.to_s).to contain "Test Query"
33+
end
34+
35+
it "outputs json format" do
36+
action.format = CB::Format::JSON
37+
expect(client).to receive(:get_saved_queries).and_return(saved_queries)
38+
action.call
39+
expect(&.output.to_s).to contain "\"name\":"
40+
end
41+
end
42+
end
43+
44+
Spectator.describe CB::SavedQueryExport do
45+
subject(action) { described_class.new client: client, output: IO::Memory.new }
46+
47+
mock_client
48+
49+
let(saved_query) { Factory.saved_query }
50+
51+
describe "#validate" do
52+
it "validates that required arguments are present" do
53+
expect_missing_arg_error
54+
action.cluster_id = "pkdpq6yynjgjbps4otxd7il2u4"
55+
expect_missing_arg_error
56+
action.query_id = "sqpvoqooxzdrriu6w3bhqo55c4"
57+
expect(&.validate).to be_true
58+
end
59+
end
60+
61+
describe "#run" do
62+
before_each do
63+
action.cluster_id = "pkdpq6yynjgjbps4otxd7il2u4"
64+
action.query_id = "sqpvoqooxzdrriu6w3bhqo55c4"
65+
end
66+
67+
it "exports to specified file" do
68+
export_path = File.join(Dir.tempdir, "test_export.sql")
69+
action.file = export_path
70+
expect(client).to receive(:get_saved_query).and_return(saved_query)
71+
action.call
72+
expect(File.read(export_path)).to eq "SELECT 1"
73+
expect(&.output.to_s).to contain "exported"
74+
File.delete(export_path)
75+
end
76+
77+
it "uses sanitized name as default filename" do
78+
expect(client).to receive(:get_saved_query).and_return(saved_query)
79+
action.call
80+
expect(File.exists?("Test_Query.sql")).to be_true
81+
expect(&.output.to_s).to contain "Test_Query.sql"
82+
File.delete("Test_Query.sql")
83+
end
84+
end
85+
end
86+
87+
Spectator.describe CB::SavedQueryImport do
88+
subject(action) { described_class.new client: client, output: IO::Memory.new }
89+
90+
mock_client
91+
92+
let(saved_query) { Factory.saved_query }
93+
94+
describe "#validate" do
95+
it "validates that required arguments are present" do
96+
expect_missing_arg_error
97+
action.cluster_id = "pkdpq6yynjgjbps4otxd7il2u4"
98+
expect_missing_arg_error
99+
action.file = File.join(Dir.tempdir, "test_import.sql")
100+
expect_missing_arg_error
101+
action.name = "My Query"
102+
expect(&.validate).to be_true
103+
end
104+
end
105+
106+
describe "#run" do
107+
before_each do
108+
action.cluster_id = "pkdpq6yynjgjbps4otxd7il2u4"
109+
action.file = File.join(Dir.tempdir, "test_import.sql")
110+
action.name = "My Query"
111+
File.write(File.join(Dir.tempdir, "test_import.sql"), "SELECT 42")
112+
end
113+
114+
after_each do
115+
path = File.join(Dir.tempdir, "test_import.sql")
116+
File.delete(path) if File.exists?(path)
117+
end
118+
119+
it "imports from file and prints confirmation" do
120+
expect(client).to receive(:create_saved_query).and_return(saved_query)
121+
action.call
122+
expect(&.output.to_s).to contain "created saved query"
123+
end
124+
end
125+
end
126+
127+
Spectator.describe CB::SavedQueryDestroy do
128+
subject(action) { described_class.new client: client, output: IO::Memory.new }
129+
130+
mock_client
131+
132+
describe "#validate" do
133+
it "validates that required arguments are present" do
134+
expect_missing_arg_error
135+
action.cluster_id = "pkdpq6yynjgjbps4otxd7il2u4"
136+
expect_missing_arg_error
137+
action.query_id = "sqpvoqooxzdrriu6w3bhqo55c4"
138+
expect(&.validate).to be_true
139+
end
140+
end
141+
142+
describe "#run" do
143+
before_each do
144+
action.cluster_id = "pkdpq6yynjgjbps4otxd7il2u4"
145+
action.query_id = "sqpvoqooxzdrriu6w3bhqo55c4"
146+
end
147+
148+
it "destroys and prints confirmation" do
149+
expect(client).to receive(:destroy_saved_query).and_return("")
150+
action.call
151+
expect(&.output.to_s).to eq "saved query destroyed\n"
152+
end
153+
end
154+
end

spec/support/factory.cr

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,4 +285,19 @@ module Factory
285285

286286
CB::Tempkey.new **params
287287
end
288+
289+
def saved_query(**params)
290+
params = {
291+
id: "sqpvoqooxzdrriu6w3bhqo55c4",
292+
name: "Test Query",
293+
sql: "SELECT 1",
294+
cluster_id: "pkdpq6yynjgjbps4otxd7il2u4",
295+
team_id: "l2gnkxjv3beifk6abkraerv7de",
296+
saved_query_folder_id: nil,
297+
created_at: Time.utc(2023, 1, 1, 0, 0, 0),
298+
updated_at: Time.utc(2023, 1, 1, 0, 0, 0),
299+
}.merge(params)
300+
301+
CB::Model::SavedQuery.new **params
302+
end
288303
end

src/cb/saved_query.cr

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
require "./action"
2+
require "./table"
3+
4+
module CB
5+
class SavedQueryList < APIAction
6+
eid_setter cluster_id
7+
format_setter format
8+
bool_setter? no_header
9+
10+
def validate
11+
check_required_args do |missing|
12+
missing << "cluster" unless cluster_id
13+
end
14+
end
15+
16+
def run
17+
validate
18+
queries = client.get_saved_queries cluster_id
19+
20+
if queries.empty?
21+
output.puts "no saved queries"
22+
return
23+
end
24+
25+
case @format
26+
when Format::JSON
27+
output << queries.to_pretty_json << '\n'
28+
else
29+
table = Table::TableBuilder.new(border: :none) do
30+
columns do
31+
add "ID"
32+
add "Name"
33+
add "Query"
34+
end
35+
36+
header unless no_header
37+
38+
queries.each do |q|
39+
row [q.id, q.name, truncate_sql(q.sql)]
40+
end
41+
end
42+
43+
output << table.render << '\n'
44+
end
45+
end
46+
47+
private def truncate_sql(sql : String?) : String
48+
return "" if sql.nil?
49+
collapsed = sql.gsub(/\s+/, " ").strip
50+
collapsed.size > 30 ? "#{collapsed[0, 50]}..." : collapsed
51+
end
52+
end
53+
54+
class SavedQueryExport < APIAction
55+
eid_setter cluster_id
56+
eid_setter query_id
57+
property file : String?
58+
59+
def validate
60+
check_required_args do |missing|
61+
missing << "cluster" unless cluster_id
62+
missing << "query" unless query_id
63+
end
64+
end
65+
66+
def run
67+
validate
68+
query = client.get_saved_query query_id
69+
70+
filename = @file || "#{query.name.gsub(/[^a-zA-Z0-9_\-]/, "_")}.sql"
71+
begin
72+
File.write(filename, query.sql)
73+
rescue e : IO::Error
74+
raise Error.new "Failed to write file '#{filename}': #{e.message}"
75+
end
76+
output << "exported " << query.name << " to " << filename << '\n'
77+
end
78+
end
79+
80+
class SavedQueryImport < APIAction
81+
eid_setter cluster_id
82+
property file : String?
83+
property name : String?
84+
85+
def validate
86+
check_required_args do |missing|
87+
missing << "cluster" unless cluster_id
88+
missing << "file" unless file
89+
missing << "name" unless name
90+
end
91+
end
92+
93+
def run
94+
validate
95+
begin
96+
sql = File.read(@file.to_s)
97+
rescue e : IO::Error
98+
raise Error.new "Failed to read file '#{@file}': #{e.message}"
99+
end
100+
101+
query = client.create_saved_query(
102+
Client::SavedQueryCreateParams.new(
103+
cluster_id: cluster_id,
104+
name: @name,
105+
sql: sql,
106+
)
107+
)
108+
109+
output << "created saved query " << query.id << '\n'
110+
end
111+
end
112+
113+
class SavedQueryDestroy < APIAction
114+
eid_setter cluster_id
115+
eid_setter query_id
116+
117+
def validate
118+
check_required_args do |missing|
119+
missing << "cluster" unless cluster_id
120+
missing << "query" unless query_id
121+
end
122+
end
123+
124+
def run
125+
validate
126+
client.destroy_saved_query query_id
127+
output << "saved query destroyed" << '\n'
128+
end
129+
end
130+
end

src/cli.cr

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,41 @@ op = OptionParser.new do |parser|
928928
end
929929
end
930930

931+
parser.on("saved-query", "Manage saved queries") do
932+
parser.banner = "cb saved-query <list|export|import|destroy>"
933+
934+
parser.on("list", "List saved queries for a cluster") do
935+
list = set_action SavedQueryList
936+
parser.banner = "cb saved-query list <--cluster>"
937+
parser.on("--cluster ID", "Choose cluster") { |arg| list.cluster_id = arg }
938+
parser.on("--format FORMAT", "Choose output format (default: table)") { |arg| list.format = arg }
939+
parser.on("--no-header", "Do not display table header") { list.no_header = true }
940+
end
941+
942+
parser.on("export", "Export a saved query to a .sql file") do
943+
export = set_action SavedQueryExport
944+
parser.banner = "cb saved-query export <--cluster> <--query>"
945+
parser.on("--cluster ID", "Choose cluster") { |arg| export.cluster_id = arg }
946+
parser.on("--query ID", "Saved query ID") { |arg| export.query_id = arg }
947+
parser.on("--file PATH", "Output file path (default: <name>.sql)") { |arg| export.file = arg }
948+
end
949+
950+
parser.on("import", "Import a saved query from a .sql file") do
951+
import = set_action SavedQueryImport
952+
parser.banner = "cb saved-query import <--cluster> <--file> <--name>"
953+
parser.on("--cluster ID", "Choose cluster") { |arg| import.cluster_id = arg }
954+
parser.on("--file PATH", "Path to .sql file") { |arg| import.file = arg }
955+
parser.on("--name NAME", "Name for the saved query") { |arg| import.name = arg }
956+
end
957+
958+
parser.on("destroy", "Destroy a saved query") do
959+
destroy = set_action SavedQueryDestroy
960+
parser.banner = "cb saved-query destroy <--cluster> <--query>"
961+
parser.on("--cluster ID", "Choose cluster") { |arg| destroy.cluster_id = arg }
962+
parser.on("--query ID", "Saved query ID") { |arg| destroy.query_id = arg }
963+
end
964+
end
965+
931966
parser.on("suspend", "Temporarily turn off a cluster") do
932967
parser.banner = "cb suspend <cluster id>"
933968
suspend = set_action ClusterSuspend

0 commit comments

Comments
 (0)