sqlite-hub 0.16.0 → 0.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -19
- package/bin/sqlite-hub.js +1041 -224
- package/frontend/index.html +1 -0
- package/frontend/js/api.js +34 -0
- package/frontend/js/app.js +481 -32
- package/frontend/js/components/modal.js +270 -50
- package/frontend/js/components/pageHeader.js +14 -16
- package/frontend/js/components/queryHistoryPanel.js +126 -131
- package/frontend/js/components/queryResults.js +18 -1
- package/frontend/js/components/rowEditorPanel.js +42 -34
- package/frontend/js/components/sidebar.js +1 -0
- package/frontend/js/router.js +8 -0
- package/frontend/js/store.js +655 -2
- package/frontend/js/utils/jsonPreview.js +31 -0
- package/frontend/js/utils/markdownDocuments.js +248 -0
- package/frontend/js/utils/rowEditorValues.js +41 -0
- package/frontend/js/utils/tableScrollState.js +39 -0
- package/frontend/js/views/charts.js +8 -0
- package/frontend/js/views/data.js +4 -1
- package/frontend/js/views/documents.js +300 -0
- package/frontend/js/views/editor.js +2 -0
- package/frontend/js/views/settings.js +39 -3
- package/frontend/styles/components.css +19 -0
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/views.css +422 -0
- package/package.json +2 -1
- package/server/middleware/localRequestSecurity.js +84 -0
- package/server/routes/connections.js +18 -1
- package/server/routes/documents.js +163 -0
- package/server/routes/settings.js +22 -6
- package/server/server.js +18 -1
- package/server/services/nativeFileDialogService.js +168 -0
- package/server/services/sqlite/dataBrowserService.js +0 -4
- package/server/services/sqlite/exportService.js +5 -1
- package/server/services/sqlite/sqlExecutor.js +51 -2
- package/server/services/storage/appStateStore.js +313 -0
- package/server/utils/sqliteTypes.js +17 -8
- package/tests/cli-args.test.js +100 -0
- package/tests/connections-file-dialog-route.test.js +46 -0
- package/tests/copy-column-modal.test.js +97 -0
- package/tests/database-documents.test.js +85 -0
- package/tests/export-blob.test.js +46 -0
- package/tests/json-preview.test.js +49 -0
- package/tests/local-request-security.test.js +85 -0
- package/tests/markdown-documents.test.js +79 -0
- package/tests/native-file-dialog.test.js +78 -0
- package/tests/query-results-truncation.test.js +20 -0
- package/tests/row-editor-null-values.test.js +131 -0
- package/tests/settings-metadata.test.js +16 -0
- package/tests/settings-view.test.js +32 -0
- package/tests/sql-identifier-safety.test.js +9 -3
- package/tests/sql-result-limit.test.js +66 -0
- package/tests/table-scroll-state.test.js +70 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const express = require("express");
|
|
2
|
+
const Database = require("better-sqlite3");
|
|
2
3
|
const fs = require("node:fs");
|
|
3
4
|
const path = require("node:path");
|
|
4
5
|
const { route, successResponse } = require("../utils/errors");
|
|
@@ -9,6 +10,23 @@ function readAppVersion() {
|
|
|
9
10
|
return packageJson.version ?? "0.0.0";
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
function readSqliteVersion() {
|
|
14
|
+
const db = new Database(":memory:");
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
return db.prepare("SELECT sqlite_version() AS version").get().version ?? "unknown";
|
|
18
|
+
} finally {
|
|
19
|
+
db.close();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readSettingsMetadata() {
|
|
24
|
+
return {
|
|
25
|
+
appVersion: readAppVersion(),
|
|
26
|
+
sqliteVersion: readSqliteVersion(),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
12
30
|
function createSettingsRouter({ appStateStore }) {
|
|
13
31
|
const router = express.Router();
|
|
14
32
|
|
|
@@ -18,9 +36,7 @@ function createSettingsRouter({ appStateStore }) {
|
|
|
18
36
|
res.json(
|
|
19
37
|
successResponse({
|
|
20
38
|
data: appStateStore.getSettings(),
|
|
21
|
-
metadata:
|
|
22
|
-
appVersion: readAppVersion(),
|
|
23
|
-
},
|
|
39
|
+
metadata: readSettingsMetadata(),
|
|
24
40
|
})
|
|
25
41
|
);
|
|
26
42
|
})
|
|
@@ -34,9 +50,7 @@ function createSettingsRouter({ appStateStore }) {
|
|
|
34
50
|
successResponse({
|
|
35
51
|
message: "Settings updated.",
|
|
36
52
|
data: settings,
|
|
37
|
-
metadata:
|
|
38
|
-
appVersion: readAppVersion(),
|
|
39
|
-
},
|
|
53
|
+
metadata: readSettingsMetadata(),
|
|
40
54
|
})
|
|
41
55
|
);
|
|
42
56
|
})
|
|
@@ -47,4 +61,6 @@ function createSettingsRouter({ appStateStore }) {
|
|
|
47
61
|
|
|
48
62
|
module.exports = {
|
|
49
63
|
createSettingsRouter,
|
|
64
|
+
readSettingsMetadata,
|
|
65
|
+
readSqliteVersion,
|
|
50
66
|
};
|
package/server/server.js
CHANGED
|
@@ -3,6 +3,11 @@ const helmet = require("helmet");
|
|
|
3
3
|
const rateLimit = require("express-rate-limit");
|
|
4
4
|
const path = require("node:path");
|
|
5
5
|
const { errorMiddleware } = require("./utils/errors");
|
|
6
|
+
const {
|
|
7
|
+
LOOPBACK_HOST,
|
|
8
|
+
listenOnLoopback,
|
|
9
|
+
localRequestSecurity,
|
|
10
|
+
} = require("./middleware/localRequestSecurity");
|
|
6
11
|
const { resolveAppStatePaths } = require("./utils/appPaths");
|
|
7
12
|
const { AppStateStore } = require("./services/storage/appStateStore");
|
|
8
13
|
const { ConnectionManager } = require("./services/sqlite/connectionManager");
|
|
@@ -10,6 +15,7 @@ const { OverviewService } = require("./services/sqlite/overviewService");
|
|
|
10
15
|
const { SqlExecutor } = require("./services/sqlite/sqlExecutor");
|
|
11
16
|
const { ImportService } = require("./services/sqlite/importService");
|
|
12
17
|
const { BackupService } = require("./services/sqlite/backupService");
|
|
18
|
+
const { NativeFileDialogService } = require("./services/nativeFileDialogService");
|
|
13
19
|
const { ExportService } = require("./services/sqlite/exportService");
|
|
14
20
|
const { StructureService } = require("./services/sqlite/structureService");
|
|
15
21
|
const { DataBrowserService } = require("./services/sqlite/dataBrowserService");
|
|
@@ -25,6 +31,7 @@ const { createTableDesignerRouter } = require("./routes/tableDesigner");
|
|
|
25
31
|
const { createMediaTaggingRouter } = require("./routes/mediaTagging");
|
|
26
32
|
const { createSettingsRouter } = require("./routes/settings");
|
|
27
33
|
const { createExportRouter } = require("./routes/export");
|
|
34
|
+
const { createDocumentsRouter } = require("./routes/documents");
|
|
28
35
|
|
|
29
36
|
const PACKAGE_ROOT = path.resolve(__dirname, "..");
|
|
30
37
|
const FRONTEND_ROOT = path.join(PACKAGE_ROOT, "frontend");
|
|
@@ -36,6 +43,7 @@ const {
|
|
|
36
43
|
legacyDatabasePaths: LEGACY_DATABASE_PATHS,
|
|
37
44
|
} = resolveAppStatePaths(PACKAGE_ROOT);
|
|
38
45
|
const DEFAULT_PORT = 4173;
|
|
46
|
+
const DEFAULT_HOST = LOOPBACK_HOST;
|
|
39
47
|
|
|
40
48
|
const appStateStore = new AppStateStore(APP_STATE_DB_PATH, {
|
|
41
49
|
legacyFilePath: LEGACY_STATE_PATH,
|
|
@@ -46,6 +54,7 @@ const overviewService = new OverviewService({ connectionManager });
|
|
|
46
54
|
const sqlExecutor = new SqlExecutor({ connectionManager, appStateStore });
|
|
47
55
|
const importService = new ImportService({ connectionManager });
|
|
48
56
|
const backupService = new BackupService({ connectionManager });
|
|
57
|
+
const nativeFileDialogService = new NativeFileDialogService();
|
|
49
58
|
const exportService = new ExportService({
|
|
50
59
|
appStateStore,
|
|
51
60
|
connectionManager,
|
|
@@ -65,6 +74,7 @@ app.use(
|
|
|
65
74
|
contentSecurityPolicy: false,
|
|
66
75
|
})
|
|
67
76
|
);
|
|
77
|
+
app.use("/api", localRequestSecurity);
|
|
68
78
|
app.use(
|
|
69
79
|
"/api",
|
|
70
80
|
rateLimit({
|
|
@@ -96,6 +106,7 @@ app.use(
|
|
|
96
106
|
connectionManager,
|
|
97
107
|
importService,
|
|
98
108
|
backupService,
|
|
109
|
+
nativeFileDialogService,
|
|
99
110
|
})
|
|
100
111
|
);
|
|
101
112
|
app.use("/api/db", createOverviewRouter({ overviewService }));
|
|
@@ -107,6 +118,7 @@ app.use("/api/table-designer", createTableDesignerRouter({ tableDesignerService
|
|
|
107
118
|
app.use("/api/media-tagging", createMediaTaggingRouter({ mediaTaggingService }));
|
|
108
119
|
app.use("/api/settings", createSettingsRouter({ appStateStore }));
|
|
109
120
|
app.use("/api/export", createExportRouter({ exportService }));
|
|
121
|
+
app.use("/api/documents", createDocumentsRouter({ appStateStore, connectionManager }));
|
|
110
122
|
|
|
111
123
|
// auth: public favicon response; it exposes no application data.
|
|
112
124
|
app.get("/favicon.ico", (req, res) => {
|
|
@@ -143,6 +155,10 @@ app.use(
|
|
|
143
155
|
"/vendor/material-symbols",
|
|
144
156
|
express.static(path.resolve(__dirname, "..", "node_modules", "material-symbols"))
|
|
145
157
|
);
|
|
158
|
+
app.use(
|
|
159
|
+
"/vendor/marked",
|
|
160
|
+
express.static(path.resolve(__dirname, "..", "node_modules", "marked"))
|
|
161
|
+
);
|
|
146
162
|
app.use(express.static(FRONTEND_ROOT));
|
|
147
163
|
app.use("/db_logos", express.static(path.join(APP_STATE_DIRECTORY, "db_logos")));
|
|
148
164
|
app.use(errorMiddleware);
|
|
@@ -185,7 +201,7 @@ function startServer({ port } = {}) {
|
|
|
185
201
|
const resolvedPort = resolvePort(port);
|
|
186
202
|
|
|
187
203
|
return new Promise((resolve, reject) => {
|
|
188
|
-
const server = app
|
|
204
|
+
const server = listenOnLoopback(app, resolvedPort);
|
|
189
205
|
|
|
190
206
|
server.once("error", reject);
|
|
191
207
|
server.once("listening", () => {
|
|
@@ -212,6 +228,7 @@ module.exports = {
|
|
|
212
228
|
app,
|
|
213
229
|
appStateStore,
|
|
214
230
|
connectionManager,
|
|
231
|
+
DEFAULT_HOST,
|
|
215
232
|
DEFAULT_PORT,
|
|
216
233
|
parsePortArgument,
|
|
217
234
|
resolvePort,
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
const path = require("node:path");
|
|
2
|
+
const os = require("node:os");
|
|
3
|
+
const { execFile } = require("node:child_process");
|
|
4
|
+
const { promisify } = require("node:util");
|
|
5
|
+
const { AppError } = require("../utils/errors");
|
|
6
|
+
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
const DEFAULT_DATABASE_FILENAME = "new-database.sqlite";
|
|
9
|
+
|
|
10
|
+
function escapePowerShellSingleQuotedString(value) {
|
|
11
|
+
return String(value).replaceAll("'", "''");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function buildDialogAttempts({ platform = process.platform, homeDirectory = os.homedir() } = {}) {
|
|
15
|
+
const defaultPath = path.join(homeDirectory, DEFAULT_DATABASE_FILENAME);
|
|
16
|
+
|
|
17
|
+
if (platform === "darwin") {
|
|
18
|
+
return [
|
|
19
|
+
{
|
|
20
|
+
command: "osascript",
|
|
21
|
+
args: [
|
|
22
|
+
"-e",
|
|
23
|
+
"on run argv",
|
|
24
|
+
"-e",
|
|
25
|
+
'set selectedFile to choose file name with prompt "Create SQLite Database" default location POSIX file (item 1 of argv) default name (item 2 of argv)',
|
|
26
|
+
"-e",
|
|
27
|
+
"return POSIX path of selectedFile",
|
|
28
|
+
"-e",
|
|
29
|
+
"end run",
|
|
30
|
+
homeDirectory,
|
|
31
|
+
DEFAULT_DATABASE_FILENAME,
|
|
32
|
+
],
|
|
33
|
+
cancelledExitCodes: new Set([1]),
|
|
34
|
+
cancelledErrorPattern: /user canceled|-128/i,
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (platform === "win32") {
|
|
40
|
+
const initialDirectory = escapePowerShellSingleQuotedString(homeDirectory);
|
|
41
|
+
const script = [
|
|
42
|
+
"Add-Type -AssemblyName System.Windows.Forms",
|
|
43
|
+
"$dialog = New-Object System.Windows.Forms.SaveFileDialog",
|
|
44
|
+
"$dialog.Title = 'Create SQLite Database'",
|
|
45
|
+
"$dialog.Filter = 'SQLite databases (*.db;*.sqlite;*.sqlite3)|*.db;*.sqlite;*.sqlite3|All files (*.*)|*.*'",
|
|
46
|
+
"$dialog.DefaultExt = 'sqlite'",
|
|
47
|
+
"$dialog.AddExtension = $true",
|
|
48
|
+
`$dialog.InitialDirectory = '${initialDirectory}'`,
|
|
49
|
+
`$dialog.FileName = '${DEFAULT_DATABASE_FILENAME}'`,
|
|
50
|
+
"if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {",
|
|
51
|
+
" [Console]::Out.Write($dialog.FileName)",
|
|
52
|
+
"} else {",
|
|
53
|
+
" exit 2",
|
|
54
|
+
"}",
|
|
55
|
+
].join("; ");
|
|
56
|
+
|
|
57
|
+
return [
|
|
58
|
+
{
|
|
59
|
+
command: "powershell.exe",
|
|
60
|
+
args: ["-NoProfile", "-STA", "-Command", script],
|
|
61
|
+
cancelledExitCodes: new Set([2]),
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return [
|
|
67
|
+
{
|
|
68
|
+
command: "zenity",
|
|
69
|
+
args: [
|
|
70
|
+
"--file-selection",
|
|
71
|
+
"--save",
|
|
72
|
+
"--title=Create SQLite Database",
|
|
73
|
+
`--filename=${defaultPath}`,
|
|
74
|
+
"--file-filter=SQLite databases | *.db *.sqlite *.sqlite3",
|
|
75
|
+
"--file-filter=All files | *",
|
|
76
|
+
],
|
|
77
|
+
cancelledExitCodes: new Set([1]),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
command: "kdialog",
|
|
81
|
+
args: [
|
|
82
|
+
"--getsavefilename",
|
|
83
|
+
defaultPath,
|
|
84
|
+
"SQLite databases (*.db *.sqlite *.sqlite3)",
|
|
85
|
+
"--title",
|
|
86
|
+
"Create SQLite Database",
|
|
87
|
+
],
|
|
88
|
+
cancelledExitCodes: new Set([1]),
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeSelectedDatabasePath(value) {
|
|
94
|
+
const selectedPath = String(value ?? "").trim();
|
|
95
|
+
|
|
96
|
+
if (!selectedPath) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return path.extname(selectedPath) ? selectedPath : `${selectedPath}.sqlite`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isMissingDialogCommand(error) {
|
|
104
|
+
return error?.code === "ENOENT";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isCancelledDialog(error, attempt) {
|
|
108
|
+
if (!attempt.cancelledExitCodes.has(Number(error?.code))) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!attempt.cancelledErrorPattern) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return attempt.cancelledErrorPattern.test(`${error?.message ?? ""}\n${error?.stderr ?? ""}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
class NativeFileDialogService {
|
|
120
|
+
constructor(options = {}) {
|
|
121
|
+
this.platform = options.platform ?? process.platform;
|
|
122
|
+
this.homeDirectory = options.homeDirectory ?? os.homedir();
|
|
123
|
+
this.executeFile = options.executeFile ?? execFileAsync;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async chooseCreateDatabasePath() {
|
|
127
|
+
const attempts = buildDialogAttempts({
|
|
128
|
+
platform: this.platform,
|
|
129
|
+
homeDirectory: this.homeDirectory,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
for (const attempt of attempts) {
|
|
133
|
+
try {
|
|
134
|
+
const result = await this.executeFile(attempt.command, attempt.args, {
|
|
135
|
+
maxBuffer: 64 * 1024,
|
|
136
|
+
windowsHide: true,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return normalizeSelectedDatabasePath(result?.stdout);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (isCancelledDialog(error, attempt)) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (isMissingDialogCommand(error)) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
throw new AppError("The native file dialog could not be opened.", 500, {
|
|
150
|
+
code: "NATIVE_FILE_DIALOG_FAILED",
|
|
151
|
+
details: { platform: this.platform },
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
throw new AppError("No supported native file dialog is available on this system.", 501, {
|
|
157
|
+
code: "NATIVE_FILE_DIALOG_UNAVAILABLE",
|
|
158
|
+
details: { platform: this.platform },
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = {
|
|
164
|
+
DEFAULT_DATABASE_FILENAME,
|
|
165
|
+
NativeFileDialogService,
|
|
166
|
+
buildDialogAttempts,
|
|
167
|
+
normalizeSelectedDatabasePath,
|
|
168
|
+
};
|
|
@@ -74,10 +74,6 @@ function formatPreviewValue(value) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
function isUnchangedSubmittedValue(currentValue, submittedValue) {
|
|
77
|
-
if (currentValue === null && submittedValue === "") {
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
77
|
return formatPreviewValue(currentValue) === formatPreviewValue(submittedValue);
|
|
82
78
|
}
|
|
83
79
|
|
|
@@ -91,6 +91,8 @@ class ExportService {
|
|
|
91
91
|
})
|
|
92
92
|
);
|
|
93
93
|
const result = this.sqlExecutor.execute(sql, {
|
|
94
|
+
blobMode: "full",
|
|
95
|
+
maxRows: null,
|
|
94
96
|
persistHistory: false,
|
|
95
97
|
requireReader: true,
|
|
96
98
|
});
|
|
@@ -132,7 +134,9 @@ class ExportService {
|
|
|
132
134
|
.filter(Boolean)
|
|
133
135
|
.join(" ")
|
|
134
136
|
);
|
|
135
|
-
const rows = serializeRows(statement.all(...(filter?.params ?? []))
|
|
137
|
+
const rows = serializeRows(statement.all(...(filter?.params ?? [])), {
|
|
138
|
+
blobMode: "full",
|
|
139
|
+
});
|
|
136
140
|
const columns = statement.columns().map((column) => column.name);
|
|
137
141
|
const content = renderExportContent({
|
|
138
142
|
columns,
|
|
@@ -2,6 +2,45 @@ const { ValidationError, mapSqliteError } = require("../../utils/errors");
|
|
|
2
2
|
const { serializeRows } = require("../../utils/sqliteTypes");
|
|
3
3
|
const { getTableDetail } = require("./introspection");
|
|
4
4
|
|
|
5
|
+
const DEFAULT_RESULT_ROW_LIMIT = 5000;
|
|
6
|
+
|
|
7
|
+
function normalizeResultRowLimit(value) {
|
|
8
|
+
if (value === null || value === false) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const normalized = value === undefined ? DEFAULT_RESULT_ROW_LIMIT : Number(value);
|
|
13
|
+
|
|
14
|
+
if (!Number.isInteger(normalized) || normalized < 1) {
|
|
15
|
+
throw new ValidationError("SQL result row limit must be a positive integer or null.");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return normalized;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readResultRows(prepared, rowLimit) {
|
|
22
|
+
if (rowLimit === null) {
|
|
23
|
+
return {
|
|
24
|
+
rows: prepared.all(),
|
|
25
|
+
truncated: false,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const rows = [];
|
|
30
|
+
let truncated = false;
|
|
31
|
+
|
|
32
|
+
for (const row of prepared.iterate()) {
|
|
33
|
+
if (rows.length >= rowLimit) {
|
|
34
|
+
truncated = true;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
rows.push(row);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { rows, truncated };
|
|
42
|
+
}
|
|
43
|
+
|
|
5
44
|
function getSerializedMemoryBytes(value) {
|
|
6
45
|
try {
|
|
7
46
|
return Buffer.byteLength(JSON.stringify(value ?? null), "utf8");
|
|
@@ -365,6 +404,7 @@ class SqlExecutor {
|
|
|
365
404
|
const db = this.connectionManager.getActiveDatabase();
|
|
366
405
|
const connection = this.connectionManager.getActiveConnection();
|
|
367
406
|
const statements = splitSqlStatements(sql);
|
|
407
|
+
const rowLimit = normalizeResultRowLimit(options.maxRows);
|
|
368
408
|
|
|
369
409
|
if (statements.length === 0) {
|
|
370
410
|
throw new ValidationError("No executable SQL statements were found.");
|
|
@@ -386,9 +426,11 @@ class SqlExecutor {
|
|
|
386
426
|
}
|
|
387
427
|
|
|
388
428
|
if (prepared.reader) {
|
|
389
|
-
const rows = prepared
|
|
429
|
+
const { rows, truncated } = readResultRows(prepared, rowLimit);
|
|
390
430
|
const columnDefinitions = prepared.columns();
|
|
391
|
-
const serializedRows = serializeRows(rows
|
|
431
|
+
const serializedRows = serializeRows(rows, {
|
|
432
|
+
blobMode: options.blobMode,
|
|
433
|
+
});
|
|
392
434
|
const editableResult = resolveEditableResult(db, columnDefinitions, serializedRows);
|
|
393
435
|
const columns = columnDefinitions.map((column) => column.name);
|
|
394
436
|
const result = {
|
|
@@ -397,6 +439,8 @@ class SqlExecutor {
|
|
|
397
439
|
keyword,
|
|
398
440
|
kind: "resultSet",
|
|
399
441
|
rowCount: serializedRows.length,
|
|
442
|
+
truncated,
|
|
443
|
+
rowLimit,
|
|
400
444
|
columns,
|
|
401
445
|
rows: editableResult?.rows ?? serializedRows,
|
|
402
446
|
editing: editableResult
|
|
@@ -463,6 +507,8 @@ class SqlExecutor {
|
|
|
463
507
|
rows: lastResultSet?.rows ?? [],
|
|
464
508
|
columns: lastResultSet?.columns ?? [],
|
|
465
509
|
editing: lastResultSet?.editing ?? null,
|
|
510
|
+
truncated: Boolean(lastResultSet?.truncated),
|
|
511
|
+
rowLimit: lastResultSet ? lastResultSet.rowLimit : null,
|
|
466
512
|
affectedRowCount: totalChanges,
|
|
467
513
|
resultKind: lastResultSet ? "resultSet" : results.at(-1)?.kind ?? "unknown",
|
|
468
514
|
};
|
|
@@ -490,6 +536,9 @@ class SqlExecutor {
|
|
|
490
536
|
}
|
|
491
537
|
|
|
492
538
|
module.exports = {
|
|
539
|
+
DEFAULT_RESULT_ROW_LIMIT,
|
|
493
540
|
SqlExecutor,
|
|
541
|
+
normalizeResultRowLimit,
|
|
542
|
+
readResultRows,
|
|
494
543
|
splitSqlStatements,
|
|
495
544
|
};
|