Skip to content
This repository was archived by the owner on Dec 19, 2022. It is now read-only.

Commit e60c848

Browse files
committed
Query execution
1 parent 742bc98 commit e60c848

File tree

6 files changed

+131
-2
lines changed

6 files changed

+131
-2
lines changed

build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ dependencies {
2727
implementation('org.springframework.boot:spring-boot-starter-security')
2828

2929
implementation('org.jetbrains:annotations:23.0.0')
30+
implementation('com.opencsv:opencsv:5.7.1')
3031

3132
compileOnly 'org.projectlombok:lombok'
3233
runtimeOnly 'org.postgresql:postgresql'

src/main/java/com/scriptql/api/domain/entities/Query.java

+3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ public class Query extends BaseEntity {
3838
@JsonIgnore
3939
private byte[] result;
4040

41+
@Nullable
42+
private String error;
43+
4144
@Nullable
4245
@JsonProperty("execution_date")
4346
@Column(name = "execution_date")
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package com.scriptql.api.domain.enums;
22

33
public enum QueryStatus {
4+
45
WAITING_REVIEW,
56
APPROVED,
67
REJECTED,
78
EXECUTING,
8-
DONE
9+
DONE,
10+
ERROR
11+
912
}

src/main/java/com/scriptql/api/rest/controllers/QueryController.java

+11
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.scriptql.api.domain.request.CreateQueryRequest;
88
import com.scriptql.api.services.QueryService;
99
import org.springframework.http.HttpStatus;
10+
import org.springframework.http.ResponseEntity;
1011
import org.springframework.web.bind.annotation.*;
1112

1213
import javax.validation.Valid;
@@ -43,4 +44,14 @@ public List<Review> getReviews(@PathVariable("id") long id) {
4344
return this.service.getReviews(id);
4445
}
4546

47+
@PostMapping("/{id:\\d+}/execute")
48+
public void execute(@PathVariable("id") long id) {
49+
this.service.execute(id);
50+
}
51+
52+
@GetMapping("/{id:\\d+}/download")
53+
public ResponseEntity<byte[]> download(@PathVariable("id") long id) {
54+
return this.service.download(id);
55+
}
56+
4657
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package com.scriptql.api.services;
2+
3+
import com.opencsv.CSVWriter;
4+
import com.scriptql.api.domain.entities.DatabaseConnection;
5+
import com.scriptql.api.domain.entities.Query;
6+
import com.scriptql.api.domain.enums.DatabaseDriver;
7+
import com.scriptql.api.domain.enums.QueryStatus;
8+
import com.scriptql.api.domain.repositories.QueryRepository;
9+
import org.springframework.stereotype.Service;
10+
11+
import java.io.ByteArrayOutputStream;
12+
import java.io.IOException;
13+
import java.io.OutputStreamWriter;
14+
import java.sql.*;
15+
import java.util.concurrent.ExecutorService;
16+
import java.util.concurrent.Executors;
17+
18+
@Service
19+
public class ExecutionService {
20+
21+
private final QueryRepository repository;
22+
private final ExecutorService scheduler = Executors.newCachedThreadPool();
23+
24+
public ExecutionService(QueryRepository repository) {
25+
this.repository = repository;
26+
}
27+
28+
public void execute(Query query) {
29+
this.scheduler.submit(() -> this.onExecute(query));
30+
}
31+
32+
private void onExecute(Query query) {
33+
try (Connection connection = this.open(query.getConnection())) {
34+
String sql = query.getQuery();
35+
boolean isSelect = sql.toUpperCase().startsWith("SELECT");
36+
if (isSelect) {
37+
connection.setReadOnly(true);
38+
}
39+
try (PreparedStatement ps = connection.prepareStatement(sql)) {
40+
if (isSelect) {
41+
try (ResultSet rs = ps.executeQuery()) {
42+
ByteArrayOutputStream stream = new ByteArrayOutputStream();
43+
try (CSVWriter writer = new CSVWriter(new OutputStreamWriter(stream))) {
44+
writer.writeAll(rs, true, false, true);
45+
}
46+
query.setResult(stream.toByteArray());
47+
}
48+
} else {
49+
int result = ps.executeUpdate();
50+
ByteArrayOutputStream stream = new ByteArrayOutputStream();
51+
try (CSVWriter writer = new CSVWriter(new OutputStreamWriter(stream))) {
52+
writer.writeNext(new String[]{"updated", String.valueOf(result)});
53+
}
54+
query.setResult(stream.toByteArray());
55+
}
56+
}
57+
query.setStatus(QueryStatus.DONE);
58+
} catch (SQLException | IOException e) {
59+
query.setStatus(QueryStatus.ERROR);
60+
query.setError(e.getMessage());
61+
} finally {
62+
query.setExecutionDate(System.currentTimeMillis());
63+
query.setUpdatedAt(System.currentTimeMillis());
64+
this.repository.save(query);
65+
}
66+
}
67+
68+
private Connection open(DatabaseConnection details) throws SQLException {
69+
String url;
70+
if (details.getDriver() == DatabaseDriver.MYSQL) {
71+
url = String.format("jdbc:mysql://%s:%s/%s",
72+
details.getHost(), details.getPort(), details.getDatabase());
73+
} else {
74+
url = String.format("jdbc:postgresql://%s:%s/%s",
75+
details.getHost(), details.getPort(), details.getDatabase());
76+
}
77+
return DriverManager.getConnection(url, details.getUsername(), details.getPassword());
78+
}
79+
80+
}

src/main/java/com/scriptql/api/services/QueryService.java

+32-1
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
import com.scriptql.api.domain.request.CreateQueryRequest;
1616
import org.jetbrains.annotations.NotNull;
1717
import org.springframework.data.jpa.domain.Specification;
18+
import org.springframework.http.*;
1819
import org.springframework.stereotype.Service;
1920

21+
import java.nio.charset.StandardCharsets;
2022
import java.util.ArrayList;
2123
import java.util.List;
2224
import java.util.Random;
@@ -30,6 +32,7 @@ public class QueryService {
3032
private final RoleRepository roles;
3133
private final ReviewRepository reviews;
3234
private final NotificationService notification;
35+
private final ExecutionService executor;
3336

3437
private final SpecBuilder<Query> builder = new SpecBuilder<>();
3538

@@ -38,13 +41,14 @@ public QueryService(
3841
ConnectionService connections,
3942
UserRoleRepository userRoles,
4043
RoleRepository roles, ReviewRepository reviews,
41-
NotificationService notification) {
44+
NotificationService notification, ExecutionService executor) {
4245
this.repository = repository;
4346
this.connections = connections;
4447
this.userRoles = userRoles;
4548
this.roles = roles;
4649
this.reviews = reviews;
4750
this.notification = notification;
51+
this.executor = executor;
4852
this.builder.addMatcher("database.name", SpecMatcher.SEARCH);
4953
this.builder.addMatcher("description", SpecMatcher.SEARCH);
5054
}
@@ -97,4 +101,31 @@ public QueryService(
97101
return this.reviews.findAllByQuery(query);
98102
}
99103

104+
public void execute(long id) {
105+
Query query = this.findById(id);
106+
if (query.getStatus() != QueryStatus.APPROVED) {
107+
throw new UserError("Unable to execute this query");
108+
}
109+
query.setStatus(QueryStatus.EXECUTING);
110+
this.repository.save(query);
111+
this.executor.execute(query);
112+
}
113+
114+
public ResponseEntity<byte[]> download(long id) {
115+
Query query = this.findById(id);
116+
if (query.getStatus() != QueryStatus.DONE) {
117+
throw new UserError("This query has not executed yet");
118+
}
119+
byte[] result = query.getResult();
120+
if (result == null || result.length == 0) {
121+
return ResponseEntity.noContent().build();
122+
}
123+
HttpHeaders headers = new HttpHeaders();
124+
headers.setContentType(MediaType.parseMediaType("text/csv"));
125+
headers.setContentDisposition(ContentDisposition.attachment()
126+
.filename(id + ".csv", StandardCharsets.UTF_8)
127+
.build());
128+
return new ResponseEntity<>(result, headers, HttpStatus.OK);
129+
}
130+
100131
}

0 commit comments

Comments
 (0)