view example/OnClientExample.java @ 970:f90d811e97eb default tip

Adjust getTableTypes() test for new table type: LOCAL TEMPORARY VIEW, added in 11.53.4 (Mar2025-SP1)
author Martin van Dinther <martin.van.dinther@monetdbsolutions.com>
date Thu, 03 Apr 2025 15:01:33 +0200 (32 hours ago)
parents d416e9b6b3d0
children
line wrap: on
line source
/*
 * SPDX-License-Identifier: MPL-2.0
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0.  If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * Copyright 2024, 2025 MonetDB Foundation;
 * Copyright August 2008 - 2023 MonetDB B.V.;
 * Copyright 1997 - July 2008 CWI.
 */

import org.monetdb.jdbc.MonetConnection;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLNonTransientException;
import java.sql.Statement;

public class OnClientExample {

	public static void main(String[] args) {
		int status;
		try {
			// Ideally this would not be hardcoded..
			final String dbUrl = "jdbc:monetdb://localhost:50000/demo";
			final String userName = "monetdb";
			final String password = "monetdb";
			final String uploadDir = "/home/jvr/mydata";
			final boolean filesAreUtf8 = false;
			String[] queries = {
					"DROP TABLE IF EXISTS mytable",
					"CREATE TABLE mytable(i INT, t TEXT)",

					// with this filename our handler will make up data by itself
					"COPY INTO mytable FROM 'generated.csv' ON CLIENT",

					// with this filename our handler will access the file system
					"COPY INTO mytable FROM 'data.csv' ON CLIENT",

					// with this statement, the server will ask the handler to stop halfway
					"COPY 20 RECORDS OFFSET 5 INTO mytable FROM 'generated.csv' ON CLIENT",

					// this demonstrates sending errors
					"COPY INTO mytable FROM 'nonexistentfilethatdoesnotexist.csv' ON CLIENT",

					// downloads but does not write to file, only counts the lines
					"COPY SELECT * FROM mytable INTO 'justcount.csv' ON CLIENT",

					// downloads to actual file
					"COPY SELECT * FROM mytable INTO 'download.csv' ON CLIENT",

					// demonstrate that the connection is still alive
					"SELECT COUNT(*) FROM mytable",
			};

			status = run(dbUrl, userName, password, uploadDir, filesAreUtf8, queries);

		} catch (Exception e) {
			status = 1;
			e.printStackTrace();
		}
		System.exit(status);
	}

	private static int run(String dbUrl, String userName, String password, String uploadDir, boolean filesAreUtf8, String[] queries) throws ClassNotFoundException, SQLException {
		int status = 0;

		// Connect
		Class.forName("org.monetdb.jdbc.MonetDriver");
		Connection conn = DriverManager.getConnection(dbUrl, userName, password);

		// Register upload- and download handler
		MyHandler handler = new MyHandler(uploadDir, filesAreUtf8);
		MonetConnection monetConnection = conn.unwrap(MonetConnection.class);
		monetConnection.setUploadHandler(handler);
		monetConnection.setDownloadHandler(handler);

		Statement stmt = conn.createStatement();
		for (String q : queries) {
			System.out.println(q);
			try {
				boolean hasResultSet = stmt.execute(q);
				if (hasResultSet) {
					ResultSet rs = stmt.getResultSet();
					long count = 0;
					while (rs.next()) {
						count++;
					}
					rs.close();
					System.out.printf("  OK, returned %d rows%n", count);
				} else {
					System.out.printf("  OK, updated %d rows%n", stmt.getUpdateCount());
				}
			} catch (SQLNonTransientException e) {
				throw e;
			} catch (SQLException e) {
				System.out.println("  => SQL ERROR " + e.getMessage());
				status = 1;
			}
		}

		return status;
	}


	private static class MyHandler implements MonetConnection.UploadHandler, MonetConnection.DownloadHandler {
		private final Path uploadDir;
		private final boolean filesAreUtf8;
		private boolean stopUploading = false;

		public MyHandler(String uploadDir, boolean filesAreUtf8) {
			this.uploadDir = FileSystems.getDefault().getPath(uploadDir).normalize();
			this.filesAreUtf8 = filesAreUtf8;
		}

		@Override
		public void uploadCancelled() {
			System.out.println("  CANCELLATION CALLBACK: server cancelled the upload");
			stopUploading = true;
		}

		@Override
		public void handleUpload(MonetConnection.Upload handle, String name, boolean textMode, long linesToSkip) throws IOException {
			// We can upload data read from the file system but also make up our own data
			if (name.equals("generated.csv")) {
				uploadGeneratedData(handle, linesToSkip);
			} else {
				uploadFileData(handle, name, textMode, linesToSkip);
			}
		}

		@Override
		public void handleDownload(MonetConnection.Download handle, String name, boolean textMode) throws IOException {
			if (name.equals("justcount.csv")) {
				justCountLines(handle);
			} else {
				downloadFileData(handle, name, textMode);
			}
		}

		private Path securelyResolvePath(String name) {
			Path p = uploadDir.resolve(name).normalize();
			if (p.startsWith(uploadDir)) {
				return p;
			} else {
				return null;
			}
		}

		private void uploadGeneratedData(MonetConnection.Upload handle, long toSkip) throws IOException {
			// Set the chunk size to a tiny amount, so we can demonstrate
			// cancellation handling. The default chunk size is one megabyte.
			// DO NOT DO THIS IN PRODUCTION!
			handle.setChunkSize(50);

			// Make up some data and upload it.
			PrintStream stream = handle.getStream();
			long n = 100;
			System.out.printf("  HANDLER: uploading %d generated lines, numbered %d to %d%n", n - toSkip, toSkip + 1, n);
			long i;
			for (i = toSkip + 1; i <= n; i++) {
				if (stopUploading) {
					System.out.printf("  HANDLER: at line %d we noticed the server asked us to stop sending%n", i);
					break;
				}
				stream.printf("%d|the number is %d%n", i, i);
			}
			System.out.println("  HANDLER: done uploading");
			stream.close();
		}

		private void uploadFileData(MonetConnection.Upload handle, String name, boolean textMode, long linesToSkip) throws IOException {
			// Validate the path, demonstrating two ways of dealing with errors
			Path path = securelyResolvePath(name);
			if (path == null || !Files.exists(path)) {
				// This makes the COPY command fail but keeps the connection
				// alive. Can only be used if we haven't sent any data yet
				handle.sendError("Invalid path");
				return;
			}
			if (!Files.isReadable(path)) {
				// As opposed to handle.sendError(), we can throw an IOException
				// at any time. Unfortunately, the file upload protocol does not
				// provide a way to indicate to the server that the data sent so
				// far is incomplete, so for the time being throwing an
				// IOException from {@handleUpload} terminates the connection.
				throw new IOException("Unreadable: " + path);
			}

			boolean binary = !textMode;
			if (binary) {
				uploadAsBinary(handle, path);
			} else if (linesToSkip == 0 && filesAreUtf8) {
				// Avoid unnecessary UTF-8 -> Java String -> UTF-8 conversions
				// by pretending the data is binary.
				uploadAsBinary(handle, path);
			} else {
				// Charset and skip handling are really necessary
				uploadAsText(handle, path, linesToSkip);
			}
		}

		private void uploadAsText(MonetConnection.Upload handle, Path path, long toSkip) throws IOException {
			BufferedReader reader = Files.newBufferedReader(path);// Converts from system encoding to Java text
			for (long i = 0; i < toSkip; i++) {
				reader.readLine();
			}
			// This variant of uploadFrom takes a Reader
			handle.uploadFrom(reader); // Converts from Java text to UTF-8 as required by MonetDB
		}

		private void uploadAsBinary(MonetConnection.Upload handle, Path path) throws IOException {
			// No charset conversion whatsoever..
			// Use this for binary data or when you are certain the file is UTF-8 encoded.
			InputStream stream = Files.newInputStream(path);
			// This variant of uploadFrom takes a Stream
			handle.uploadFrom(stream);
		}

		private void justCountLines(MonetConnection.Download handle) throws IOException {
			System.out.println("  HANDLER: not writing the download to file, just counting the lines");
			InputStream stream = handle.getStream();
			InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8); // MonetDB always sends UTF-8
			BufferedReader bufreader = new BufferedReader(reader);
			long count = 0;
			while (bufreader.readLine() != null) {
				count++;
			}
			System.out.println("  HANDLER: file had " + count + " lines");
		}

		private void downloadFileData(MonetConnection.Download handle, String name, boolean textMode) throws IOException {
			Path path = securelyResolvePath(name);
			if (path == null) {
				handle.sendError("Illegal path");
				return;
			}

			OutputStream stream = Files.newOutputStream(path);
			if (!textMode || filesAreUtf8) {
				handle.downloadTo(stream);
				stream.close(); // do not forget this
			} else {
				OutputStreamWriter writer = new OutputStreamWriter(stream, Charset.defaultCharset()); // let system decide the encoding
				BufferedWriter bufwriter = new BufferedWriter(writer);
				handle.downloadTo(bufwriter);
				bufwriter.close(); // do not forget this
			}
		}
	}
}