├── .github └── workflows │ ├── jdkversions.yml │ ├── macos.yml │ └── monetdbversions.yml ├── .hgignore ├── .hgtags ├── ChangeLog ├── ChangeLog-Archive ├── Makefile ├── README.md ├── SQLSTATEs ├── build.properties ├── build.xml ├── example ├── MJDBCTest.java ├── OnClientExample.java ├── PreparedExample.java └── SQLImport.java ├── license.txt ├── pom.xml ├── release.txt ├── src └── main │ └── java │ └── org │ └── monetdb │ ├── client │ ├── JMonetDB.java │ └── JdbcClient.java │ ├── jdbc │ ├── MonetBlob.java │ ├── MonetCallableStatement.java │ ├── MonetClob.java │ ├── MonetConnection.java │ ├── MonetDataSource.java │ ├── MonetDatabaseMetaData.java │ ├── MonetDriver.java │ ├── MonetParameterMetaData.java │ ├── MonetPreparedStatement.java │ ├── MonetResultSet.java │ ├── MonetResultSetMetaData.java │ ├── MonetSavepoint.java │ ├── MonetStatement.java │ ├── MonetVersion.java.in │ ├── MonetWrapper.java │ └── types │ │ ├── INET.java │ │ └── URL.java │ ├── mcl │ ├── MCLException.java │ ├── io │ │ ├── BufferedMCLReader.java │ │ ├── BufferedMCLWriter.java │ │ └── LineType.java │ ├── net │ │ ├── ClientInfo.java │ │ ├── HandshakeOption.java │ │ ├── MapiSocket.java │ │ ├── MonetUrlParser.java │ │ ├── Parameter.java │ │ ├── ParameterType.java │ │ ├── SecureSocket.java │ │ ├── Target.java │ │ └── ValidationError.java │ └── parser │ │ ├── HeaderLineParser.java │ │ ├── MCLParseException.java │ │ ├── MCLParser.java │ │ ├── StartOfHeaderParser.java │ │ └── TupleLineParser.java │ ├── merovingian │ ├── Control.java │ ├── MerovingianException.java │ └── SabaothDB.java │ └── util │ ├── CmdLineOpts.java │ ├── Exporter.java │ ├── Extract.java │ ├── FileTransferHandler.java │ ├── MDBvalidator.java │ ├── OptionsException.java │ ├── SQLExporter.java │ ├── SQLRestore.java │ └── XMLExporter.java ├── tests ├── ConnectionTests.java ├── JDBC_API_Tester.java ├── OnClientTester.java ├── SQLcopyinto.java ├── TLSTester.java ├── Test_Cforkbomb.java ├── Test_Csendthread.java ├── UrlTester.java ├── build.properties ├── build.xml ├── drop.sql ├── javaspecific.md └── tests.md └── version.sh /.github/workflows/jdkversions.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | # or https://github.com/actions/checkout and https://github.com/actions/setup-java 4 | 5 | # Note: Oracle JDK is only supported for JDK 17, 21 and later 6 | 7 | name: Test with various JDK versions 8 | 9 | on: [push] 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | distribution: 19 | - liberica 20 | - oracle 21 | java_version: 22 | - 8 23 | - 11 24 | - 17 25 | - 21 26 | - 22 27 | - 23 28 | - 24 29 | exclude: 30 | - distribution: oracle 31 | java_version: 8 32 | - distribution: oracle 33 | java_version: 11 34 | 35 | services: 36 | monetdb_container: 37 | image: monetdb/monetdb:latest 38 | env: 39 | MDB_DAEMON_PASS: monetdb 40 | MDB_DB_ADMIN_PASS: monetdb 41 | ports: 42 | - 50000:50000 43 | 44 | steps: 45 | - uses: actions/checkout@v4 46 | 47 | - name: Set up JDK 48 | uses: actions/setup-java@v4 49 | with: 50 | java-version: ${{ matrix.java_version }} 51 | distribution: ${{ matrix.distribution }} 52 | 53 | - name: Build 54 | run: make 55 | 56 | - name: Create jar symlink 57 | run: ln -s monetdb-jdbc*.jar monetdb-jdbc.jar 58 | working-directory: jars 59 | 60 | - name: Run JDBC_API_Tester 61 | run: java -cp jars/monetdb-jdbc.jar:jars/jdbctests.jar JDBC_API_Tester 'jdbc:monetdb://localhost:50000/monetdb?user=monetdb&password=monetdb' 62 | 63 | - name: Run OnClientTester 64 | run: java -cp jars/monetdb-jdbc.jar:jars/jdbctests.jar OnClientTester 'jdbc:monetdb://localhost:50000/monetdb?user=monetdb&password=monetdb' -v 65 | -------------------------------------------------------------------------------- /.github/workflows/macos.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | # or https://github.com/actions/checkout and https://github.com/actions/setup-java 4 | 5 | name: MacOS 6 | 7 | on: [push] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up JDK 1.8 17 | uses: actions/setup-java@v4 18 | with: 19 | java-version: 8 20 | distribution: liberica 21 | 22 | - name: Build 23 | run: make 24 | 25 | - name: Create jar symlink 26 | run: ln -s monetdb-jdbc*.jar monetdb-jdbc.jar 27 | working-directory: jars 28 | 29 | # running java test programs fails, due to failed connection. Probably the server was not started. Assistance needed. 30 | # - name: Run JDBC_API_Tester 31 | # run: java -cp jars/monetdb-jdbc.jar:jars/jdbctests.jar JDBC_API_Tester 'jdbc:monetdb://localhost:50000/monetdb?user=monetdb&password=monetdb' 32 | 33 | # - name: Run OnClientTester 34 | # run: java -cp jars/monetdb-jdbc.jar:jars/jdbctests.jar OnClientTester 'jdbc:monetdb://localhost:50000/monetdb?user=monetdb&password=monetdb' -v 35 | -------------------------------------------------------------------------------- /.github/workflows/monetdbversions.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | # or https://github.com/actions/checkout and https://github.com/actions/setup-java 4 | 5 | name: Test with various MonetDB versions 6 | 7 | on: [push] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | monetdbversion: 17 | - "monetdb/dev-builds:default" 18 | - "monetdb/monetdb:latest" 19 | - "monetdb/monetdb:Mar2025" 20 | - "monetdb/dev-builds:Aug2024" 21 | - "monetdb/dev-builds:Dec2023" 22 | - "monetdb/dev-builds:Jun2023" 23 | - "monetdb/dev-builds:Sep2022" 24 | - "monetdb/dev-builds:Jan2022" 25 | 26 | services: 27 | monetdb_container: 28 | image: ${{ matrix.monetdbversion }} 29 | env: 30 | MDB_DAEMON_PASS: monetdb 31 | MDB_DB_ADMIN_PASS: monetdb 32 | ports: 33 | - 50000:50000 34 | 35 | steps: 36 | - uses: actions/checkout@v4 37 | 38 | - name: Set up JDK 39 | uses: actions/setup-java@v4 40 | with: 41 | java-version: 8 42 | distribution: liberica 43 | 44 | - name: Build 45 | run: make 46 | 47 | - name: Create jar symlink 48 | run: ln -s monetdb-jdbc*.jar monetdb-jdbc.jar 49 | working-directory: jars 50 | 51 | - name: Run JDBC_API_Tester 52 | run: java -cp jars/monetdb-jdbc.jar:jars/jdbctests.jar JDBC_API_Tester 'jdbc:monetdb://localhost:50000/monetdb?user=monetdb&password=monetdb' 53 | 54 | - name: Run OnClientTester 55 | run: java -cp jars/monetdb-jdbc.jar:jars/jdbctests.jar OnClientTester 'jdbc:monetdb://localhost:50000/monetdb?user=monetdb&password=monetdb' -v 56 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | 3 | # files created by the normal build process 4 | *.class 5 | *.jar 6 | build/META-INF/services/java.sql.Driver 7 | src/main/java/org/monetdb/jdbc/MonetVersion.java 8 | 9 | # files generated by various editors 10 | *.swp 11 | *~ 12 | \#* 13 | .#* 14 | 15 | # other files we don't want 16 | TAGS 17 | tags 18 | *.rej 19 | *.orig 20 | -------------------------------------------------------------------------------- /.hgtags: -------------------------------------------------------------------------------- 1 | 80de05f07508fec938845b4a6e05f600bf0b48c0 v2.24 2 | c43c293f3d5841517cbe0d858108c4da5fb1ec0c v2.26 3 | a6a2f4ee2d42d7e192f9d8d37f79ea99178d7f2c v2.25 4 | fe8170e2b549c22ceb2d96301022b9304f62424d v2.27 5 | 6423fb0bf9ebba64167ca1b40b79aeacbb8e7c47 v2.28 6 | 70808ab71d2ad997d7b7fa38d675fc90f71a059c v2.29 7 | bc39810b3faa4c52ce63b29147075e91266a3e84 v3.0 8 | a80b5db244b8ceeea936770b9c5f314d46251fb3 v3.1 9 | fd66573f88946b488e4f1f26db624d8f370aa5a8 v3.2 10 | 843f7d03540ab1170ce80d9e89993235b45d3ff9 v3.3 11 | 1a4014a0c0c8751c5a5ea9e1205f1aea22c9188b v12.0 12 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | # ChangeLog file for monetdb-java 2 | # This file is updated with Maddlog 3 | 4 | * Thu Feb 13 2025 Martin van Dinther 5 | - Corrected the returned integer values of Statement methods 6 | getUpdateCount() and getLargeUpdateCount(). They returned -2 for 7 | DDL statements, which was not in compliance with the JDBC API 8 | documentation. Now they no longer return numbers smaller than -1. 9 | - Corrected the returned integer values of Statement methods 10 | executeUpdate(...) and executeLargeUpdate(...) and PreparedStatement 11 | methods executeUpdate() and executeLargeUpdate(). They returned -2 for 12 | DDL statements, which was not in compliance with the JDBC API 13 | documentation. Now they no longer return negative numbers. 14 | 15 | * Wed Feb 12 2025 Martin van Dinther 16 | - Fix a bug in DatabaseMetaData.getTables() where a provided string in the 17 | array of table types argument would contain a single quote or back slash. 18 | It was not escaped properly, resulting in an SQL syntax error. 19 | - Fix missing escaping of single back slashes in string data provided to 20 | PreparedStatement methods setString(), setClob(), setObject() and setURL(). 21 | 22 | * Thu Jan 16 2025 Martin van Dinther 23 | - The release version number has been bumped to 12.0 to avoid confusion 24 | with historic 11.x versions. 25 | - Compiled and released new jar files: monetdb-jdbc-12.0.jre8.jar and 26 | jdbcclient.jre8.jar 27 | 28 | * Wed Jan 8 2025 Joeri van Ruth 29 | - The JDBC jar now includes JdbcClient. For backward compatibility the jar 30 | is still also available as jdbcclient.jreX.jar. 31 | 32 | * Thu Nov 7 2024 Martin van Dinther 33 | - In JdbcClient when running the \vsci or \vdbi or \vsi commands, we now 34 | suppress "42000 SELECT: insufficient privileges for ..." and 35 | "42000 SELECT: access denied for ..." error messages when the connected user 36 | does not have 'monetdb' or 'sysadmin' privileges, needed for some validations. 37 | 38 | * Wed Jun 19 2024 Joeri van Ruth 39 | - Implemented Connection#set/getClientInfo, and send sensible default info 40 | at connect time. This can be controlled with the properties 'client_info=on/off', 41 | 'client_application=ApplicationName' and 'client_remark=Other Useful Info'. 42 | Note setting client info requires MonetDB server 11.51 (Aug2024) or higher. 43 | 44 | * Thu Apr 4 2024 Martin van Dinther 45 | - Corrected ResultSetMetaData methods getColumnTypeName(), getPrecision(), 46 | getColumnDisplaySize() and ParameterMetaData methods getParameterTypeName() 47 | and getPrecision() for the interval data types. They now return 48 | more precise information for the 13 possible interval data types. 49 | 50 | * Thu Mar 7 2024 Martin van Dinther 51 | - Improved DatabaseMetaData.getTypeInfo(). It now also returns the serial 52 | and bigserial data types and all 13 possible interval data types. 53 | 54 | * Fri Jan 5 2024 Joeri van Ruth 55 | - Network connections can now be encrypted with TLS by using jdbc:monetdbs://.. 56 | instead of jdbc:monetdb://. The server is authenticated using the JVM's root 57 | certificate pool unless cert= or certhash= properties are given. 58 | - The syntax of the JDBC URL's has been updated to match the monetdb:// and 59 | monetdbs:// URL syntax introduced in MonetDB 11.51 (Aug2024), see 60 | https://www.monetdb.org/documentation/user-guide/client-interfaces/monetdb-urls/. 61 | This adds a number of properties that can be set in the URL but is otherwise 62 | backward compatible except that percent sign are now used to escape other characters. 63 | For example, the password '100%milk&cookies' can be passed as 64 | password=100%25milk%26cookies. 65 | 66 | * Thu Dec 28 2023 Martin van Dinther 67 | - In ResultSet.getObject(column, Class type) and 68 | ResultSet.getObject(column, Map>) methods added support 69 | to return objects for classes: java.time.LocalDate, java.time.LocalDateTime 70 | and java.time.LocalTime. 71 | 72 | * Wed Dec 20 2023 Martin van Dinther 73 | - Enhanced DatabaseMetaData.getTables() method by adding support for SQL 74 | table type names: 'BASE TABLE', 'GLOBAL TEMPORARY' and 'LOCAL TEMPORARY' 75 | in parameter types[]. These are SQL synonyms of MonetDB table type names: 76 | 'TABLE', 'GLOBAL TEMPORARY TABLE' and 'LOCAL TEMPORARY TABLE'. 77 | 78 | * Thu Dec 14 2023 Martin van Dinther 79 | - In ResultSet.getObject() method added support for retrieving 80 | TIMESTAMP WITH TIME ZONE data as java.time.OffsetDateTime object 81 | and TIME WITH TIME ZONE as java.time.OffsetTime object. 82 | Also methods ResultSetMetaData.getColumnClassName() and 83 | ParameterMetaData.getParameterClassName() now return 84 | java.time.OffsetDateTime.class for columns of type TIMESTAMP WITH TIME ZONE 85 | and java.time.OffsetTime.class for columns of type TIME WITH TIME ZONE. 86 | 87 | * Thu Oct 12 2023 Martin van Dinther 88 | - Improved DatabaseMetaData.getSystemFunctions(). It now also returns 89 | functions: current_sessionid, greatest, ifnull, least, sql_max, sql_min. 90 | Function ifnull will only be returned for servers Jun2023 (11.47 or higher). 91 | 92 | * Wed Aug 9 2023 Martin van Dinther 93 | - Implemented ResultSet methods: 94 | T getObject(int columnIndex, Class type) 95 | T getObject(String columnLabel, Class type) 96 | They used to throw an SQLFeatureNotSupportedException. 97 | 98 | * Thu Jul 6 2023 Martin van Dinther 99 | - Removed deprecated nl.cwi.monetdb.*.* classes and package. 100 | Those classes were marked deprecated on 12 Nov 2020 from 101 | release 3.0 (released on 17 Feb 2021) onwards. It includes: 102 | nl.cwi.monetdb.client.JdbcClient.class 103 | nl.cwi.monetdb.jdbc.MonetDriver.class 104 | nl.cwi.monetdb.jdbc.types.INET.class 105 | nl.cwi.monetdb.jdbc.types.URL.class 106 | nl.cwi.monetdb.mcl.net.MapiSocket.class 107 | These classes are now removed permanently. 108 | Use the org.monetdb.* equivalents instead. 109 | 110 | * Thu Jul 6 2023 Martin van Dinther 111 | - Removed code to support old MonetDB servers Oct2014 or older. Those 112 | old servers did not yet have the system tables: sys.keywords and 113 | sys.table_types which are introduced in Jul2015 release. Those system 114 | tables are used by MonetDatabaseMetaData methods: getSQLKeywords(), 115 | getTableTypes() and getTables(). These 3 methods will now fail when 116 | used with those very old MonetDB servers. 117 | 118 | * Wed Jul 5 2023 Martin van Dinther 119 | - Corrected implementation of Connection methods getClientInfo() and 120 | setClientInfo(). They used to get/set Connection properties instead of 121 | Client Info properties, which was incorrect. 122 | 123 | * Thu Jun 22 2023 Martin van Dinther 124 | - Corrected DatabaseMetaData method getClientProperties(). 125 | It used to return connection properties instead of client info properties. 126 | 127 | * Thu May 4 2023 Martin van Dinther 128 | - Corrected DatabaseMetaData method getStringFunctions() when connected 129 | to Jun2023 server. It now includes the string functions from the new 130 | txtsim module. 131 | 132 | For a complete list of changes in previous monetdb-java releases see: 133 | https://www.monetdb.org/downloads/Java/archive/ChangeLog-Archive 134 | 135 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: src/main/java/org/monetdb/jdbc/MonetVersion.java.in 2 | ant -f build.xml distjdbc distmerocontrol 3 | cd tests; ant -f build.xml jar_jdbctests 4 | 5 | jre17jars: src/main/java/org/monetdb/jdbc/MonetVersion.java 6 | rm -rf build 7 | ant -f build.xml -Djvm.version=17 distjdbc 8 | rm -rf build 9 | 10 | jre21jars: src/main/java/org/monetdb/jdbc/MonetVersion.java 11 | rm -rf build 12 | ant -f build.xml -Djvm.version=21 distjdbc 13 | rm -rf build 14 | 15 | test: all 16 | cd tests; ant -f build.xml test 17 | 18 | testsjar: 19 | cd tests; ant -f build.xml jar_jdbctests 20 | 21 | doc: 22 | ant -f build.xml doc 23 | 24 | clean: 25 | rm -f src/main/java/org/monetdb/jdbc/MonetVersion.java 26 | rm -rf build tests/build jars doc 27 | 28 | cleandoc: 29 | rm -rf doc 30 | 31 | cleantests: 32 | rm -rf tests/build 33 | rm -f jars/jdbctests.jar 34 | 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## MonetDB-java 2 | 3 | This repository contains the Java source code of the official `MonetDB JDBC driver`, 4 | the `MonetDB JdbcClient program`, and some Java test and example programs. 5 | 6 | The master repository is: [hg monetdb-java](https://www.monetdb.org/hg/monetdb-java/file/tip). 7 | 8 | A read-only copy is available on: [github monetdb-java](https://github.com/MonetDB/monetdb-java). 9 | 10 | 11 | These Java programs are designed to work with the [MonetDB Database System](https://www.monetdb.org/). 12 | They support MonetDB servers from version 11.21 (Jul2015) and higher. 13 | However only the latest MonetDB server versions are tested actively. 14 | 15 | The `MonetDB JDBC driver` allows Java programs to connect to a MonetDB 16 | database server using standard, database independent Java code. 17 | It is an open source JDBC driver implementing JDBC API 4.2, written in Pure Java (Type 4), 18 | and communicates in the MonetDB native network protocol. 19 | 20 | The sources are actively maintained by the MonetDB team at [MonetDB Solutions](https://www.monetdbsolutions.com/). 21 | 22 | The latest released jar files can be downloaded from [MonetDB Java Download Area](https://www.monetdb.org/downloads/Java/). 23 | 24 | ## Tools 25 | To build the jar files yourself, you need `JDK 8` (or higher), `ant` and `make` tools installed. 26 | 27 | ## Build Process 28 | To build, simply run `make` command from a shell. 29 | See contents of `Makefile` for other `make` targets, such as `make test` or `make doc`. 30 | 31 | The `.class` files will be stored in the `build/` and `tests/build/` subdirectories. 32 | The `.jar` files will be stored in the `jars/` subdirectory. 33 | 34 | By default debug symbols are **not** included in the compiled class and jar files. 35 | To include debug symbols, edit file `build.properties` change line `enable_debug=true` and rebuild. 36 | 37 | ## Tests 38 | To test, simply run `make test` command from a shell. 39 | 40 | **Note** For the tests to succeed you first have to startup a MonetDB server (on localhost, port 50000). 41 | 42 | ## JDBC Driver 43 | The MonetDB JDBC driver consists of one single jar file: `monetdb-jdbc-##.#.jre8.jar`. 44 | 45 | We recommend to always use the [latest released jar file](https://www.monetdb.org/downloads/Java/). 46 | The latest released JDBC driver can be downloaded from [MonetDB Java Download Area](https://www.monetdb.org/downloads/Java/). 47 | 48 | See [JDBC driver info](https://www.monetdb.org/documentation/user-guide/client-interfaces/libraries-drivers/jdbc-driver/) for more info. 49 | 50 | ## JdbcClient program 51 | The `JdbcClient program` is an interactive program using a command-line interface (CLI), similar to the mclient program. 52 | It consists of one single jar file: [jdbcclient.jre8.jar](https://www.monetdb.org/downloads/Java/) and includes and uses the MonetDB JDBC driver. 53 | 54 | The `JdbcClient program` can be started via shell commands: 55 | ``` 56 | cd jars 57 | 58 | java -jar jdbcclient.jre8.jar 59 | ``` 60 | 61 | To get a list of JdbcClient startup options simply execute: 62 | ``` 63 | java -jar jdbcclient.jre8.jar --help 64 | ``` 65 | 66 | See [JdbcClient doc](https://www.monetdb.org/documentation/user-guide/client-interfaces/jdbcclient/) for more info. 67 | 68 | ## Reporting issues 69 | Before reporting an issue, please check if you have used the [latest released jar files](https://www.monetdb.org/downloads/Java/). 70 | Some issues may already have been fixed in the latest released jar files. 71 | 72 | If you find a bug in the latest released jar files or have a request, please log it as an issue at: 73 | [github monetdb-java issues](https://github.com/MonetDB/monetdb-java/issues). 74 | Include which versions of the released JDBC driver and MonetDB server you are using and on which platforms. 75 | For bugs also include a small standalone java reproduction program. 76 | 77 | **Note** we do not accept Pull requests on Github as it is a read-only copy. 78 | 79 | ## Copyright Notice 80 | SPDX-License-Identifier: MPL-2.0 81 | 82 | This Source Code Form is subject to the terms of the Mozilla Public 83 | License, v. 2.0. If a copy of the MPL was not distributed with this 84 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 85 | 86 | Copyright 2024, 2025 MonetDB Foundation; 87 | Copyright August 2008 - 2023 MonetDB B.V.; 88 | Copyright 1997 - July 2008 CWI. 89 | -------------------------------------------------------------------------------- /SQLSTATEs: -------------------------------------------------------------------------------- 1 | Following SQLStates are MonetDB JDBC driver specific: 2 | 01M02 redirect warning 3 | 01M03 illegal arguments (invalid call of internal function) 4 | 01M07 unrecognised clientinfo property 5 | 01M08 read-only connection mode not supported 6 | 01M09 transaction mode not supported 7 | 01M10 unexpected server output 8 | 01M11 server-client autocommit state mismatch 9 | 01M13 concurrency mode not supported 10 | 01M14 scrolling mode not supported 11 | 01M15 holdability mode not supported 12 | 01M18 generated keys for columns not supported 13 | 01M21 cursors not supported 14 | 01M22 JDBC escape syntax not supported 15 | 01M23 field size limitation not supported 16 | 01M24 query time out not supported (not used/needed anymore) 17 | 18 | 08M01 opening logfile failed 19 | 08M26 invalid URI 20 | 08M33 connection timed out 21 | 22 | 0AM21 cursors not supported 23 | 0AM34 Java generics not supported 24 | 25 | 22M28 invalid BLOB format 26 | 22M29 invalid inet format 27 | 22M30 invalid URL format 28 | 22M31 invalid UUID format 29 | 22M32 invalid JSON format 30 | 22M33 invalid Date format 31 | 22M34 invalid Time format 32 | 22M35 invalid Timestamp format 33 | 22M36 invalid Time with time zone format 34 | 22M37 invalid Timestamp with time zone format 35 | 36 | 2BM37 dependent objects still exist 37 | 2DM30 autocommit mode active 38 | 3BM30 autocommit mode active 39 | 40 | 42M31 user/role already exists 41 | 42M32 user/role not found 42 | 42M35 sequence not found 43 | 42M36 cannot restart sequence with NULL 44 | 45 | M0M03 illegal arguments (invalid call of internal function) 46 | M0M04 only supported in SQL mode 47 | M0M06 savepoint is not MonetSavepoint 48 | M0M10 protocol violation/unexpected server response 49 | M0M12 matching client handle referenced by server not found 50 | M0M27 unknown error 51 | M0M29 assert 52 | 53 | M1M05 invalid argument (user supplied) 54 | M1M16 multistatements not supported in batches 55 | M1M17 result set not expected for DML or DDL-statement 56 | M1M19 response is not a result set 57 | M1M20 object closed 58 | M1M25 failed reading from/writing to object stream 59 | 60 | SQLState codes are used in SQLExceptions. 61 | JDBC 4.2 defines the following SQLException subclass mappings: 62 | NonTransientSQLExceptions (fails when same operation executed again) 63 | 0A SQLFeatureNotSupportedException 64 | 08 SQLNonTransientConnectionException 65 | 22 SQLDataException 66 | 23 SQLIntegrityConstraintViolationException 67 | 28 SQLInvalidAuthorizationSpecException 68 | 42 SQLSyntaxErrorException 69 | TransientSQLExeceptions (retry of same operation might succeed) 70 | 08 SQLTransientConnectionException 71 | 40 SQLTransactionRollbackException 72 | null SQLTimeoutException 73 | 74 | See also: http://docs.oracle.com/javase/8/docs/api/java/sql/SQLException.html 75 | See also: https://en.wikipedia.org/wiki/SQLSTATE 76 | 77 | -------------------------------------------------------------------------------- /build.properties: -------------------------------------------------------------------------------- 1 | # Default build parameters. These may be overridden by local configuration 2 | # settings in build.local.properties. 3 | 4 | 5 | ## 6 | ## JDBC (and client) 7 | ## 8 | 9 | # major release number 10 | JDBC_MAJOR=12 11 | # minor release number 12 | JDBC_MINOR=1 13 | 14 | 15 | ## 16 | ## General 17 | ## 18 | 19 | # should we compile with debugging symbols? Not for public releases 20 | enable_debug=false 21 | 22 | # should we optimize the build, preferably not for debugging. Yes for public releases 23 | enable_optimize=true 24 | -------------------------------------------------------------------------------- /example/MJDBCTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | import java.sql.*; 14 | 15 | /** 16 | * This example assumes there exists tables a and b filled with some data. 17 | * On these tables some queries are executed and the JDBC driver is tested 18 | * on it's accuracy and robustness against 'users'. 19 | * 20 | * @author Fabian Groffen 21 | */ 22 | public class MJDBCTest { 23 | public static void main(String[] args) throws Exception { 24 | String MonetDB_JDBC_URL = "jdbc:monetdb://localhost:50000/demo"; // change host, port and databasename 25 | Connection con = null; 26 | try { 27 | con = DriverManager.getConnection(MonetDB_JDBC_URL, "monetdb", "monetdb"); 28 | } catch (SQLException e) { 29 | System.err.println("Failed to connect to MonetDB server! Message: " + e.getMessage()); 30 | } 31 | 32 | if (con == null) { 33 | System.err.println("Failed to create a connection object!"); 34 | return; 35 | } 36 | 37 | Statement st = con.createStatement(); 38 | ResultSet rs; 39 | 40 | String sql = "SELECT a.var1, COUNT(b.id) as total FROM a, b WHERE a.var1 = b.id AND a.var1 = 'andb' GROUP BY a.var1 ORDER BY a.var1, total;"; 41 | rs = st.executeQuery(sql); 42 | // get meta data and print columns with their type 43 | ResultSetMetaData md = rs.getMetaData(); 44 | for (int i = 1; i <= md.getColumnCount(); i++) { 45 | System.out.print(md.getColumnName(i) + ":" + 46 | md.getColumnTypeName(i) + "\t"); 47 | } 48 | System.out.println(""); 49 | // print the data: only the first 5 rows, while there probably are 50 | // a lot more. This shouldn't cause any problems afterwards since the 51 | // result should get properly discarded on the next query 52 | for (int i = 0; rs.next() && i < 5; i++) { 53 | for (int j = 1; j <= md.getColumnCount(); j++) { 54 | System.out.print(rs.getString(j) + "\t"); 55 | } 56 | System.out.println(""); 57 | } 58 | 59 | // tell the driver to only return 5 rows, it can optimize on this 60 | // value, and will not fetch any more than 5 rows. 61 | st.setMaxRows(5); 62 | // we ask the database for 22 rows, while we set the JDBC driver to 63 | // 5 rows, this shouldn't be a problem at all... 64 | rs = st.executeQuery("select * from a limit 22"); 65 | int var1_cnr = rs.findColumn("var1"); 66 | int var2_cnr = rs.findColumn("var2"); 67 | int var3_cnr = rs.findColumn("var3"); 68 | int var4_cnr = rs.findColumn("var4"); 69 | // read till the driver says there are no rows left 70 | for (int i = 0; rs.next(); i++) { 71 | System.out.println( 72 | "[" + rs.getString(var1_cnr) + "]" + 73 | "[" + rs.getString(var2_cnr) + "]" + 74 | "[" + rs.getInt(var3_cnr) + "]" + 75 | "[" + rs.getString(var4_cnr) + "]" ); 76 | } 77 | 78 | // this rs.close is not needed, should be done by next execute(Query) call 79 | // however if there can be some time between this point and the next 80 | // execute call, it is from a resource perspective better to close it. 81 | rs.close(); 82 | 83 | // unset the row limit; 0 means as much as the database sends us 84 | st.setMaxRows(0); 85 | // we only ask 10 rows 86 | rs = st.executeQuery("select * from b limit 10;"); 87 | int rowid_cnr = rs.findColumn("rowid"); 88 | int id_cnr = rs.findColumn("id"); 89 | var1_cnr = rs.findColumn("var1"); 90 | var2_cnr = rs.findColumn("var2"); 91 | var3_cnr = rs.findColumn("var3"); 92 | var4_cnr = rs.findColumn("var4"); 93 | // and simply print them 94 | while (rs.next()) { 95 | System.out.println( 96 | rs.getInt(rowid_cnr) + ", " + 97 | rs.getString(id_cnr) + ", " + 98 | rs.getInt(var1_cnr) + ", " + 99 | rs.getInt(var2_cnr) + ", " + 100 | rs.getString(var3_cnr) + ", " + 101 | rs.getString(var4_cnr) ); 102 | } 103 | 104 | // this close is not required, as the Statement will close the last 105 | // ResultSet around when it's closed 106 | // again, if that can take some time, it's nicer to close immediately 107 | // the reason why these closes are commented out here, is to test if 108 | // the driver really cleans up it's mess like it should 109 | rs.close(); 110 | 111 | // perform a ResultSet-less query (with no trailing ; since that should 112 | // be possible as well and is JDBC standard) 113 | // Note that this method should return the number of updated rows. This 114 | // method however always returns -1, since Monet currently doesn't 115 | // support returning the affected rows. 116 | st.executeUpdate("delete from a where var1 = 'zzzz'"); 117 | 118 | // Don't forget to do it yourself if the connection is reused or much 119 | // longer alive, since the Statement object contains a lot of things 120 | // you probably want to reclaim if you don't need them anymore. 121 | st.close(); 122 | // closing the connection should take care of closing all generated 123 | // statements from it... 124 | con.close(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /example/OnClientExample.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | import org.monetdb.jdbc.MonetConnection; 14 | 15 | import java.io.BufferedReader; 16 | import java.io.BufferedWriter; 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | import java.io.InputStreamReader; 20 | import java.io.OutputStream; 21 | import java.io.OutputStreamWriter; 22 | import java.io.PrintStream; 23 | import java.nio.charset.Charset; 24 | import java.nio.charset.StandardCharsets; 25 | import java.nio.file.FileSystems; 26 | import java.nio.file.Files; 27 | import java.nio.file.Path; 28 | import java.sql.Connection; 29 | import java.sql.DriverManager; 30 | import java.sql.ResultSet; 31 | import java.sql.SQLException; 32 | import java.sql.SQLNonTransientException; 33 | import java.sql.Statement; 34 | 35 | public class OnClientExample { 36 | 37 | public static void main(String[] args) { 38 | int status; 39 | try { 40 | // Ideally this would not be hardcoded.. 41 | final String dbUrl = "jdbc:monetdb://localhost:50000/demo"; 42 | final String userName = "monetdb"; 43 | final String password = "monetdb"; 44 | final String uploadDir = "/home/jvr/mydata"; 45 | final boolean filesAreUtf8 = false; 46 | String[] queries = { 47 | "DROP TABLE IF EXISTS mytable", 48 | "CREATE TABLE mytable(i INT, t TEXT)", 49 | 50 | // with this filename our handler will make up data by itself 51 | "COPY INTO mytable FROM 'generated.csv' ON CLIENT", 52 | 53 | // with this filename our handler will access the file system 54 | "COPY INTO mytable FROM 'data.csv' ON CLIENT", 55 | 56 | // with this statement, the server will ask the handler to stop halfway 57 | "COPY 20 RECORDS OFFSET 5 INTO mytable FROM 'generated.csv' ON CLIENT", 58 | 59 | // this demonstrates sending errors 60 | "COPY INTO mytable FROM 'nonexistentfilethatdoesnotexist.csv' ON CLIENT", 61 | 62 | // downloads but does not write to file, only counts the lines 63 | "COPY SELECT * FROM mytable INTO 'justcount.csv' ON CLIENT", 64 | 65 | // downloads to actual file 66 | "COPY SELECT * FROM mytable INTO 'download.csv' ON CLIENT", 67 | 68 | // demonstrate that the connection is still alive 69 | "SELECT COUNT(*) FROM mytable", 70 | }; 71 | 72 | status = run(dbUrl, userName, password, uploadDir, filesAreUtf8, queries); 73 | 74 | } catch (Exception e) { 75 | status = 1; 76 | e.printStackTrace(); 77 | } 78 | System.exit(status); 79 | } 80 | 81 | private static int run(String dbUrl, String userName, String password, String uploadDir, boolean filesAreUtf8, String[] queries) throws ClassNotFoundException, SQLException { 82 | int status = 0; 83 | 84 | // Connect 85 | Class.forName("org.monetdb.jdbc.MonetDriver"); 86 | Connection conn = DriverManager.getConnection(dbUrl, userName, password); 87 | 88 | // Register upload- and download handler 89 | MyHandler handler = new MyHandler(uploadDir, filesAreUtf8); 90 | MonetConnection monetConnection = conn.unwrap(MonetConnection.class); 91 | monetConnection.setUploadHandler(handler); 92 | monetConnection.setDownloadHandler(handler); 93 | 94 | Statement stmt = conn.createStatement(); 95 | for (String q : queries) { 96 | System.out.println(q); 97 | try { 98 | boolean hasResultSet = stmt.execute(q); 99 | if (hasResultSet) { 100 | ResultSet rs = stmt.getResultSet(); 101 | long count = 0; 102 | while (rs.next()) { 103 | count++; 104 | } 105 | rs.close(); 106 | System.out.printf(" OK, returned %d rows%n", count); 107 | } else { 108 | System.out.printf(" OK, updated %d rows%n", stmt.getUpdateCount()); 109 | } 110 | } catch (SQLNonTransientException e) { 111 | throw e; 112 | } catch (SQLException e) { 113 | System.out.println(" => SQL ERROR " + e.getMessage()); 114 | status = 1; 115 | } 116 | } 117 | 118 | return status; 119 | } 120 | 121 | 122 | private static class MyHandler implements MonetConnection.UploadHandler, MonetConnection.DownloadHandler { 123 | private final Path uploadDir; 124 | private final boolean filesAreUtf8; 125 | private boolean stopUploading = false; 126 | 127 | public MyHandler(String uploadDir, boolean filesAreUtf8) { 128 | this.uploadDir = FileSystems.getDefault().getPath(uploadDir).normalize(); 129 | this.filesAreUtf8 = filesAreUtf8; 130 | } 131 | 132 | @Override 133 | public void uploadCancelled() { 134 | System.out.println(" CANCELLATION CALLBACK: server cancelled the upload"); 135 | stopUploading = true; 136 | } 137 | 138 | @Override 139 | public void handleUpload(MonetConnection.Upload handle, String name, boolean textMode, long linesToSkip) throws IOException { 140 | // We can upload data read from the file system but also make up our own data 141 | if (name.equals("generated.csv")) { 142 | uploadGeneratedData(handle, linesToSkip); 143 | } else { 144 | uploadFileData(handle, name, textMode, linesToSkip); 145 | } 146 | } 147 | 148 | @Override 149 | public void handleDownload(MonetConnection.Download handle, String name, boolean textMode) throws IOException { 150 | if (name.equals("justcount.csv")) { 151 | justCountLines(handle); 152 | } else { 153 | downloadFileData(handle, name, textMode); 154 | } 155 | } 156 | 157 | private Path securelyResolvePath(String name) { 158 | Path p = uploadDir.resolve(name).normalize(); 159 | if (p.startsWith(uploadDir)) { 160 | return p; 161 | } else { 162 | return null; 163 | } 164 | } 165 | 166 | private void uploadGeneratedData(MonetConnection.Upload handle, long toSkip) throws IOException { 167 | // Set the chunk size to a tiny amount, so we can demonstrate 168 | // cancellation handling. The default chunk size is one megabyte. 169 | // DO NOT DO THIS IN PRODUCTION! 170 | handle.setChunkSize(50); 171 | 172 | // Make up some data and upload it. 173 | PrintStream stream = handle.getStream(); 174 | long n = 100; 175 | System.out.printf(" HANDLER: uploading %d generated lines, numbered %d to %d%n", n - toSkip, toSkip + 1, n); 176 | long i; 177 | for (i = toSkip + 1; i <= n; i++) { 178 | if (stopUploading) { 179 | System.out.printf(" HANDLER: at line %d we noticed the server asked us to stop sending%n", i); 180 | break; 181 | } 182 | stream.printf("%d|the number is %d%n", i, i); 183 | } 184 | System.out.println(" HANDLER: done uploading"); 185 | stream.close(); 186 | } 187 | 188 | private void uploadFileData(MonetConnection.Upload handle, String name, boolean textMode, long linesToSkip) throws IOException { 189 | // Validate the path, demonstrating two ways of dealing with errors 190 | Path path = securelyResolvePath(name); 191 | if (path == null || !Files.exists(path)) { 192 | // This makes the COPY command fail but keeps the connection 193 | // alive. Can only be used if we haven't sent any data yet 194 | handle.sendError("Invalid path"); 195 | return; 196 | } 197 | if (!Files.isReadable(path)) { 198 | // As opposed to handle.sendError(), we can throw an IOException 199 | // at any time. Unfortunately, the file upload protocol does not 200 | // provide a way to indicate to the server that the data sent so 201 | // far is incomplete, so for the time being throwing an 202 | // IOException from {@handleUpload} terminates the connection. 203 | throw new IOException("Unreadable: " + path); 204 | } 205 | 206 | boolean binary = !textMode; 207 | if (binary) { 208 | uploadAsBinary(handle, path); 209 | } else if (linesToSkip == 0 && filesAreUtf8) { 210 | // Avoid unnecessary UTF-8 -> Java String -> UTF-8 conversions 211 | // by pretending the data is binary. 212 | uploadAsBinary(handle, path); 213 | } else { 214 | // Charset and skip handling are really necessary 215 | uploadAsText(handle, path, linesToSkip); 216 | } 217 | } 218 | 219 | private void uploadAsText(MonetConnection.Upload handle, Path path, long toSkip) throws IOException { 220 | BufferedReader reader = Files.newBufferedReader(path);// Converts from system encoding to Java text 221 | for (long i = 0; i < toSkip; i++) { 222 | reader.readLine(); 223 | } 224 | // This variant of uploadFrom takes a Reader 225 | handle.uploadFrom(reader); // Converts from Java text to UTF-8 as required by MonetDB 226 | } 227 | 228 | private void uploadAsBinary(MonetConnection.Upload handle, Path path) throws IOException { 229 | // No charset conversion whatsoever.. 230 | // Use this for binary data or when you are certain the file is UTF-8 encoded. 231 | InputStream stream = Files.newInputStream(path); 232 | // This variant of uploadFrom takes a Stream 233 | handle.uploadFrom(stream); 234 | } 235 | 236 | private void justCountLines(MonetConnection.Download handle) throws IOException { 237 | System.out.println(" HANDLER: not writing the download to file, just counting the lines"); 238 | InputStream stream = handle.getStream(); 239 | InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8); // MonetDB always sends UTF-8 240 | BufferedReader bufreader = new BufferedReader(reader); 241 | long count = 0; 242 | while (bufreader.readLine() != null) { 243 | count++; 244 | } 245 | System.out.println(" HANDLER: file had " + count + " lines"); 246 | } 247 | 248 | private void downloadFileData(MonetConnection.Download handle, String name, boolean textMode) throws IOException { 249 | Path path = securelyResolvePath(name); 250 | if (path == null) { 251 | handle.sendError("Illegal path"); 252 | return; 253 | } 254 | 255 | OutputStream stream = Files.newOutputStream(path); 256 | if (!textMode || filesAreUtf8) { 257 | handle.downloadTo(stream); 258 | stream.close(); // do not forget this 259 | } else { 260 | OutputStreamWriter writer = new OutputStreamWriter(stream, Charset.defaultCharset()); // let system decide the encoding 261 | BufferedWriter bufwriter = new BufferedWriter(writer); 262 | handle.downloadTo(bufwriter); 263 | bufwriter.close(); // do not forget this 264 | } 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /example/PreparedExample.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | import java.sql.*; 14 | 15 | /** 16 | * This example shows the use of the PreparedStatement 17 | * 18 | * @author Fabian Groffen 19 | */ 20 | public class PreparedExample { 21 | public static void main(String[] args) throws Exception { 22 | Connection con = DriverManager.getConnection("jdbc:monetdb://localhost/notused", "monetdb", "monetdb"); 23 | PreparedStatement st = con.prepareStatement("SELECT ? AS a1, ? AS a2"); 24 | ResultSet rs; 25 | 26 | st.setString(1, "te\\s't"); 27 | st.setInt(2, 10); 28 | 29 | rs = st.executeQuery(); 30 | // get meta data and print columns with their type 31 | ResultSetMetaData md = rs.getMetaData(); 32 | for (int i = 1; i <= md.getColumnCount(); i++) { 33 | System.out.print(md.getColumnName(i) + ":" + 34 | md.getColumnTypeName(i) + "\t"); 35 | } 36 | System.out.println(""); 37 | 38 | while (rs.next()) { 39 | for (int j = 1; j <= md.getColumnCount(); j++) { 40 | System.out.print(rs.getString(j) + "\t"); 41 | } 42 | System.out.println(""); 43 | } 44 | rs.close(); 45 | 46 | con.close(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /example/SQLImport.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | import java.sql.*; 14 | import java.io.*; 15 | 16 | /** 17 | * This simple example somewhat emulates the mclient command. However in 18 | * it's simpleness it only supports SQL queries which entirely are on one line. 19 | * 20 | * This program reads a file line by line, and feeds the line into a running 21 | * Mserver5 on the localhost. Upon error, the error is reported and the program 22 | * continues reading and executing lines. 23 | * A very lousy way of implementing options is used to somewhat configure the 24 | * behaviour of the program in order to be a bit more verbose or commit after 25 | * each (successfully) executed line. 26 | * 27 | * The program uses a debuglog in which the exact conversation between the 28 | * JDBC driver and Mserver is reported. The log file is put in the current 29 | * working directory and names like monet_[unix timestamp].log 30 | * 31 | * @author Fabian Groffen 32 | */ 33 | 34 | public class SQLImport { 35 | public static void main(String[] args) throws Exception { 36 | if (args.length < 1) { 37 | System.out.println("synopsis: java SQLImport filename [autocommit] [verbose]"); 38 | System.exit(-1); 39 | } 40 | 41 | // open the file 42 | BufferedReader fr = new BufferedReader(new FileReader(args[0])); 43 | 44 | // request a connection suitable for MonetDB from the driver manager 45 | // note that the database specifier is currently not implemented, for 46 | // MonetDB itself can't access multiple databases. 47 | // turn on debugging 48 | Connection con = DriverManager.getConnection("jdbc:monetdb://localhost/demo?debug=true", "monetdb", "monetdb"); 49 | 50 | boolean beVerbose = false; 51 | if (args.length == 3) { 52 | // turn on verbose mode 53 | beVerbose = true; 54 | } 55 | if (args.length < 2) { 56 | // disable auto commit using the driver 57 | con.setAutoCommit(false); 58 | } 59 | 60 | // get a statement to execute on 61 | Statement stmt = con.createStatement(); 62 | 63 | String query; 64 | for (int i = 1; (query = fr.readLine()) != null; i++) { 65 | if (beVerbose) 66 | System.out.println(query); 67 | try { 68 | // execute the query, no matter what it is 69 | stmt.execute(query); 70 | } catch (SQLException e) { 71 | System.out.println("Error on line " + i + ": " + e.getMessage()); 72 | if (!beVerbose) 73 | System.out.println(query); 74 | } 75 | } 76 | 77 | // free resources, close the statement 78 | stmt.close(); 79 | // close the connection with the database 80 | con.close(); 81 | // close the file 82 | fr.close(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | SPDX-License-Identifier: MPL-2.0 2 | 3 | This Source Code Form is subject to the terms of the Mozilla Public 4 | License, v. 2.0. If a copy of the MPL was not distributed with this 5 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | 7 | Copyright 2024, 2025 MonetDB Foundation; 8 | Copyright August 2008 - 2023 MonetDB B.V.; 9 | Copyright 1997 - July 2008 CWI. 10 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | monetdb 8 | monetdb-jdbc 9 | 12.0 10 | ${project.groupId}:${project.artifactId} 11 | MonetDB JDBC driver 12 | https://www.monetdb.org 13 | 14 | 15 | 16 | Mozilla Public License, Version 2.0 17 | https://www.mozilla.org/MPL/2.0/ 18 | 19 | 20 | 21 | 22 | 23 | Sjoerd Mullender 24 | sjoerd@monetdb.org 25 | MonetDB 26 | https://www.monetdb.org 27 | 28 | 29 | 30 | 31 | 32 | ossrh 33 | https://oss.sonatype.org/content/repositories/snapshots 34 | 35 | 36 | ossrh 37 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 38 | 39 | 40 | 41 | 42 | scm:hg:https://dev.monetdb.org/hg/monetdb-java/ 43 | scm:hg:ssh://hg@dev.monetdb.org/monetdb-java/ 44 | https://dev.monetdb.org/hg/monetdb-java/ 45 | 46 | 47 | 48 | UTF-8 49 | org/monetdb/jdbc/**/*.java 50 | org/monetdb/mcl/**/*.java 51 | 52 | 53 | 54 | 55 | 56 | org.apache.maven.plugins 57 | maven-compiler-plugin 58 | 3.2 59 | 60 | 61 | ${jdbc.sources} 62 | ${mcl.sources} 63 | 64 | 8 65 | 8 66 | true 67 | true 68 | 69 | 70 | 71 | org.apache.maven.plugins 72 | maven-source-plugin 73 | 2.2.1 74 | 75 | 76 | attach-sources 77 | 78 | jar-no-fork 79 | 80 | 81 | 82 | 83 | 84 | ${jdbc.sources} 85 | ${mcl.sources} 86 | 87 | 88 | 89 | 90 | org.apache.maven.plugins 91 | maven-javadoc-plugin 92 | 2.9.1 93 | 94 | 95 | attach-javadocs 96 | 97 | jar 98 | 99 | 100 | 101 | 102 | 103 | ${jdbc.sources} 104 | ${mcl.sources} 105 | 106 | -Xdoclint:none 107 | 108 | 109 | 110 | org.sonatype.plugins 111 | nexus-staging-maven-plugin 112 | 1.6.3 113 | true 114 | 115 | ossrh 116 | https://oss.sonatype.org/ 117 | false 118 | 119 | 120 | 121 | org.apache.maven.plugins 122 | maven-release-plugin 123 | 2.5 124 | 125 | true 126 | false 127 | release 128 | deploy 129 | 130 | 131 | 132 | org.apache.maven.plugins 133 | maven-gpg-plugin 134 | 1.5 135 | 136 | 137 | sign-artifacts 138 | verify 139 | 140 | sign 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /release.txt: -------------------------------------------------------------------------------- 1 | RELEASE INFO 2 | ============ 3 | 4 | MonetDB JDBC driver version 12.0 5 | Release date: 2025-01-17 6 | 7 | The Java Database Connectivity (JDBC) API provides universal data access from 8 | the Java programming language. 9 | 10 | The MonetDB JDBC driver is designed for use with MonetDB, an Open-Source column-store RDBMS. 11 | See https://www.monetdb.org/ 12 | 13 | The latest MonetDB JDBC driver can be downloaded from 14 | https://www.monetdb.org/downloads/Java/ 15 | 16 | Documentation for the MonetDB JDBC driver is available from 17 | https://www.monetdb.org/documentation/user-guide/client-interfaces/libraries-drivers/jdbc-driver/ 18 | 19 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/client/JMonetDB.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.client; 14 | 15 | import org.monetdb.merovingian.Control; 16 | import org.monetdb.merovingian.SabaothDB; 17 | 18 | import org.monetdb.util.CmdLineOpts; 19 | import org.monetdb.util.OptionsException; 20 | 21 | import java.io.PrintWriter; 22 | import java.util.ArrayList; 23 | import java.util.List; 24 | 25 | /** 26 | * This program mimics the monetdb tool. It is meant as demonstration 27 | * and test of the MeroControl library. 28 | * 29 | * @author Fabian Groffen 30 | * @version 1.0 31 | */ 32 | 33 | public final class JMonetDB { 34 | private static PrintWriter out; 35 | 36 | public final static void main(String[] args) throws Exception { 37 | final CmdLineOpts copts = new CmdLineOpts(); 38 | 39 | // arguments which take exactly one argument 40 | copts.addOption("h", "host", CmdLineOpts.CAR_ONE, "localhost", 41 | "The hostname of the host that runs the MonetDB server. " + 42 | "A port number can be supplied by use of a colon, i.e. " + 43 | "-h somehost:12345."); 44 | copts.addOption("p", "port", CmdLineOpts.CAR_ONE, "50000", 45 | "The port number to connect to."); 46 | copts.addOption("P", "passphrase", CmdLineOpts.CAR_ONE, null, 47 | "The passphrase to tell the MonetDB server"); 48 | copts.addOption("c", "command", CmdLineOpts.CAR_ONE_MANY, null, 49 | "The command to execute on the MonetDB server"); 50 | 51 | // arguments which have no argument(s) 52 | copts.addOption(null, "help", CmdLineOpts.CAR_ZERO, null, 53 | "This help screen."); 54 | 55 | // extended options 56 | copts.addOption(null, "Xhash", CmdLineOpts.CAR_ONE, null, 57 | "Use the given hash algorithm during challenge response. " + 58 | "Supported algorithm names: SHA512, SHA384, SHA256 and SHA1."); 59 | // arguments which can have zero or one argument(s) 60 | copts.addOption(null, "Xdebug", CmdLineOpts.CAR_ONE, null, 61 | "Writes a transmission log to disk for debugging purposes. " + 62 | "A file name must be given."); 63 | 64 | try { 65 | copts.processArgs(args); 66 | } catch (OptionsException e) { 67 | System.err.println("Error: " + e.getMessage()); 68 | System.exit(1); 69 | } 70 | 71 | if (copts.getOption("help").isPresent()) { 72 | System.out.print( 73 | "Usage java -jar jmonetdb.jar\n" + 74 | " -h host[:port] -p port -P passphrase [-X] -c cmd ...\n" + 75 | "or using long option equivalents --host --port --passphrase.\n" + 76 | "Arguments may be written directly after the option like -p50000.\n" + 77 | "\n" + 78 | "If no host and port are given, localhost and 50000 are assumed.\n" + 79 | "\n" + 80 | "OPTIONS\n" + copts.produceHelpMessage() 81 | ); 82 | System.exit(0); 83 | } 84 | 85 | out = new PrintWriter(new java.io.BufferedWriter(new java.io.OutputStreamWriter(System.out))); 86 | 87 | String pass = copts.getOption("passphrase").getArgument(); 88 | 89 | // we need the password from the user, fetch it with a pseudo 90 | // password protector 91 | if (pass == null) { 92 | final char[] tmp = System.console().readPassword("passphrase: "); 93 | if (tmp == null) { 94 | System.err.println("Invalid passphrase!"); 95 | System.exit(1); 96 | } 97 | pass = String.valueOf(tmp); 98 | } 99 | 100 | // build the hostname 101 | String host = copts.getOption("host").getArgument(); 102 | String sport = copts.getOption("port").getArgument(); 103 | int pos = host.indexOf(':'); 104 | if (pos != -1) { 105 | sport = host.substring(pos + 1); 106 | host = host.substring(0, pos); 107 | } 108 | int port = Integer.parseInt(sport); 109 | 110 | String hash = null; 111 | if (copts.getOption("Xhash").isPresent()) 112 | hash = copts.getOption("Xhash").getArgument(); 113 | 114 | if (!copts.getOption("command").isPresent()) { 115 | System.err.println("need a command to execute (-c)"); 116 | System.exit(1); 117 | } 118 | 119 | Control ctl = null; 120 | try { 121 | ctl = new Control(host, port, pass); 122 | } catch (IllegalArgumentException e) { 123 | System.err.println(e.getMessage()); 124 | System.exit(1); 125 | } 126 | // FIXME: Control needs to respect Xhash 127 | 128 | if (copts.getOption("Xdebug").isPresent()) { 129 | final String fname = copts.getOption("Xdebug").getArgument(); 130 | ctl.setDebug(fname); 131 | } 132 | 133 | final String[] commands = copts.getOption("command").getArguments(); 134 | if (commands[0].equals("status")) { 135 | List sdbs; 136 | if (commands.length == 1) { 137 | sdbs = ctl.getAllStatuses(); 138 | } else { 139 | sdbs = new ArrayList(); 140 | for (int i = 1; i < commands.length; i++) 141 | sdbs.add(ctl.getStatus(commands[i])); 142 | } 143 | final java.util.Iterator it = sdbs.iterator(); 144 | while (it.hasNext()) { 145 | SabaothDB sdb = it.next(); 146 | System.out.println(sdb.getName() + " " + sdb.getURI()); 147 | } 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/jdbc/MonetDataSource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.jdbc; 14 | 15 | import java.io.PrintWriter; 16 | import java.sql.Connection; 17 | import java.sql.SQLException; 18 | import java.sql.SQLFeatureNotSupportedException; 19 | import javax.sql.DataSource; 20 | import java.util.Properties; 21 | 22 | /** 23 | *
 24 |  * A DataSource suitable for the MonetDB database.
 25 |  *
 26 |  * This DataSource allows retrieval of a Connection using the JNDI bean like
 27 |  * framework. A DataSource has numerous advantages over using the DriverManager
 28 |  * to retrieve a Connection object. Using the DataSource interface enables a
 29 |  * more transparent application where the location or database can be changed
 30 |  * without changing any application code.
 31 |  *
 32 |  * Additionally, pooled connections can be used when using a DataSource.
 33 |  *
34 | * 35 | * @author Fabian Groffen 36 | * @version 0.2 37 | */ 38 | public final class MonetDataSource 39 | extends MonetWrapper 40 | implements DataSource 41 | { 42 | private String description; 43 | private int loginTimeout; 44 | private String user; 45 | // insecure, but how to do it better? 46 | private String password; 47 | private String url; 48 | 49 | // the following properties are also standard: 50 | // private String dataSourceName; 51 | // private String networkProtocol; 52 | // private String serverName; 53 | // private String role; 54 | 55 | 56 | private final MonetDriver driver; 57 | 58 | /** 59 | * Constructor of a MonetDataSource which uses default settings for a 60 | * connection. You probably want to change this setting using the 61 | * method setURL. 62 | */ 63 | public MonetDataSource() { 64 | description = "MonetDB database"; 65 | url = "jdbc:monetdb://localhost/"; 66 | 67 | driver = new MonetDriver(); 68 | } 69 | 70 | /** 71 | * Attempts to establish a connection with the data source that this 72 | * DataSource object represents. 73 | * 74 | * @return a MonetConnection 75 | * @throws SQLException if connecting to the database fails 76 | */ 77 | @Override 78 | public Connection getConnection() throws SQLException { 79 | return getConnection(user, password); 80 | } 81 | 82 | /** 83 | * Attempts to establish a connection with the data source that this 84 | * DataSource object represents. 85 | * 86 | * @param username the username to use 87 | * @param password the password to use 88 | * @return a MonetConnection 89 | * @throws SQLException if connecting to the database fails 90 | */ 91 | @Override 92 | public Connection getConnection(final String username, final String password) 93 | throws SQLException 94 | { 95 | if (loginTimeout > 0) { 96 | /// could enable Socket.setSoTimeout(int timeout) here... 97 | } 98 | final Properties props = new Properties(); 99 | props.put("user", username); 100 | props.put("password", password); 101 | 102 | return driver.connect(url, props); 103 | } 104 | 105 | 106 | /** 107 | * Gets the maximum time in seconds that this data source can wait while 108 | * attempting to connect to a database. 109 | * 110 | * @return login timeout default is 0 (infinite) 111 | */ 112 | @Override 113 | public int getLoginTimeout() { 114 | return loginTimeout; 115 | } 116 | 117 | /** 118 | * Sets the maximum time in seconds that this data source will wait while 119 | * attempting to connect to a database. 120 | * 121 | * @param seconds the number of seconds to wait before aborting the connect 122 | */ 123 | @Override 124 | public void setLoginTimeout(final int seconds) { 125 | loginTimeout = seconds; 126 | } 127 | 128 | /** 129 | * Retrieves the log writer for this DataSource object. 130 | * 131 | * @return null, since there is no log writer 132 | */ 133 | @Override 134 | public PrintWriter getLogWriter() { 135 | return null; 136 | } 137 | 138 | /** 139 | * Sets the log writer for this DataSource object to the given 140 | * java.io.PrintWriter object. 141 | * 142 | * @param out a PrintWriter - ignored 143 | */ 144 | @Override 145 | public void setLogWriter(final PrintWriter out) { 146 | } 147 | 148 | /** 149 | * Sets the password to use when connecting. There is no getter 150 | * for obvious reasons. 151 | * 152 | * @param password the password 153 | */ 154 | public void setPassword(final String password) { 155 | this.password = password; 156 | } 157 | 158 | /** 159 | * Gets the username 160 | * 161 | * @return the username 162 | */ 163 | public String getUser() { 164 | return user; 165 | } 166 | 167 | /** 168 | * Sets the username 169 | * 170 | * @param user the username 171 | */ 172 | public void setUser(final String user) { 173 | this.user = user; 174 | } 175 | 176 | /** 177 | * Gets the connection URL 178 | * 179 | * @return the connection URL 180 | */ 181 | public String getURL() { 182 | return url; 183 | } 184 | 185 | /** 186 | * Sets the connection URL 187 | * 188 | * @param url the connection URL 189 | */ 190 | public void setDatabaseName(final String url) { 191 | this.url = url; 192 | } 193 | 194 | /** 195 | * Gets the description 196 | * 197 | * @return the description 198 | */ 199 | public String getDescription() { 200 | return description; 201 | } 202 | 203 | /** 204 | * Sets the description 205 | * 206 | * @param description the description 207 | */ 208 | public void setDescription(final String description) { 209 | this.description = description; 210 | } 211 | 212 | /** 213 | * Return the parent Logger of all the Loggers used by this data 214 | * source. This should be the Logger farthest from the root Logger 215 | * that is still an ancestor of all of the Loggers used by this data 216 | * source. Configuring this Logger will affect all of the log 217 | * messages generated by the data source. In the worst case, this 218 | * may be the root Logger. 219 | * 220 | * @return the parent Logger for this data source 221 | * @throws SQLFeatureNotSupportedException if the data source does 222 | * not use java.util.logging 223 | */ 224 | @Override 225 | public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException { 226 | throw newSQLFeatureNotSupportedException("getParentLogger"); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/jdbc/MonetSavepoint.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.jdbc; 14 | 15 | import java.sql.Savepoint; 16 | import java.sql.SQLException; 17 | import java.util.concurrent.atomic.AtomicInteger; 18 | 19 | /** 20 | *
 21 |  * A {@link Savepoint} suitable for the MonetDB database.
 22 |  *
 23 |  * The representation of a savepoint, which is a point within the current
 24 |  * transaction that can be referenced from the Connection.rollback() method.
 25 |  * When a transaction is rolled back to a savepoint all changes made after
 26 |  * that savepoint are undone.
 27 |  * Savepoints can be either named or unnamed. Unnamed savepoints are
 28 |  * identified by an ID generated by the underlying data source.
 29 |  *
 30 |  * This little class is nothing more than a container for a name and/or
 31 |  * an id. Each instance of this class always has an id, which is used for
 32 |  * internal representation of the save point.
 33 |  *
 34 |  * Because the IDs which get generated are a logical sequence, application
 35 |  * wide, two concurrent transactions are guaranteed to not have the same
 36 |  * save point identifiers. In this implementation the validity of save points
 37 |  * is determined by the server, which makes this a light implementation.
 38 |  *
39 | * 40 | * @author Fabian Groffen 41 | * @version 1.1 42 | */ 43 | public final class MonetSavepoint implements Savepoint { 44 | /** The id of the last created Savepoint */ 45 | private static final AtomicInteger highestId = new AtomicInteger(0); 46 | 47 | /** The name of this Savepoint */ 48 | private final String name; 49 | /** The id of this Savepoint */ 50 | private final int id; 51 | 52 | /** 53 | * Creates a named MonetSavepoint object 54 | * 55 | * @param name of savepoint 56 | * @throws IllegalArgumentException if no or empty name is given 57 | */ 58 | public MonetSavepoint(final String name) throws IllegalArgumentException { 59 | if (name == null || name.isEmpty()) 60 | throw new IllegalArgumentException("Missing savepoint name"); 61 | 62 | this.id = getNextId(); 63 | this.name = name; 64 | } 65 | 66 | /** 67 | * Creates an unnamed MonetSavepoint object 68 | */ 69 | public MonetSavepoint() { 70 | this.id = getNextId(); 71 | this.name = null; 72 | } 73 | 74 | 75 | /** 76 | * Retrieves the generated ID for the savepoint that this Savepoint object 77 | * represents. 78 | * 79 | * @return the numeric ID of this savepoint 80 | * @throws SQLException if this is a named savepoint 81 | */ 82 | @Override 83 | public int getSavepointId() throws SQLException { 84 | if (name != null) 85 | throw new SQLException("Cannot get ID of named savepoint", "3B000"); 86 | 87 | return id; 88 | } 89 | 90 | /** 91 | * Retrieves the name of the savepoint that this Savepoint object 92 | * represents. 93 | * 94 | * @return the name of this savepoint 95 | * @throws SQLException if this is an un-named savepoint 96 | */ 97 | @Override 98 | public String getSavepointName() throws SQLException { 99 | if (name == null) 100 | throw new SQLException("Cannot get name of un-named savepoint", "3B000"); 101 | 102 | return name; 103 | } 104 | 105 | //== end of methods from Savepoint interface 106 | 107 | /** 108 | * Retrieves the savepoint id, like the getSavepointId method with the only 109 | * difference that this method will always return the id, regardless of 110 | * whether it is named or not. 111 | * 112 | * @return the numeric ID of this savepoint 113 | */ 114 | final int getId() { 115 | return id; 116 | } 117 | 118 | /** 119 | * Returns the constructed internal name to use when referencing this save point to the 120 | * MonetDB database. The returned value is guaranteed to be unique and consists of 121 | * a prefix string and a sequence number. 122 | * 123 | * @return the unique savepoint name 124 | */ 125 | final String getName() { 126 | return "JDBCSP" + id; 127 | } 128 | 129 | 130 | /** 131 | * Returns the next id, which is larger than the last returned id. This 132 | * method is synchronized to prevent race conditions. Since this is static 133 | * code, this method is shared by requests from multiple Connection objects. 134 | * Therefore two successive calls to this method need not to have a 135 | * difference of 1. 136 | * 137 | * @return the next int which is guaranteed to be higher than the one 138 | * at the last call to this method. 139 | */ 140 | private static final int getNextId() { 141 | return highestId.incrementAndGet(); 142 | } 143 | 144 | /** 145 | * Returns the highest id returned by getNextId(). This method is also 146 | * synchronized to prevent race conditions and thus guaranteed to be 147 | * thread-safe. 148 | * 149 | * @return the highest id returned by a call to getNextId() 150 | */ 151 | // private static int getHighestId() { 152 | // return highestId.get(); 153 | // } 154 | } 155 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/jdbc/MonetVersion.java.in: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.jdbc; 14 | 15 | final class MonetVersion { 16 | 17 | /** 18 | * Major version of MonetDB JDBC driver. 19 | */ 20 | public static int majorVersion = @JDBC_MAJOR@; 21 | 22 | /** 23 | * Minor version of MonetDB JDBC driver. 24 | */ 25 | public static int minorVersion = @JDBC_MINOR@; 26 | 27 | /** 28 | * Full version string of MonetDB JDBC driver. 29 | */ 30 | public static String driverVersion = "@JDBC_MAJOR@.@JDBC_MINOR@"; 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/jdbc/MonetWrapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.jdbc; 14 | 15 | import java.sql.SQLException; 16 | import java.sql.SQLFeatureNotSupportedException; 17 | 18 | /** 19 | *
 20 |  * A Wrapper class which provide the ability to retrieve the delegate instance
 21 |  * when the instance in question is in fact a proxy class.
 22 |  *
 23 |  * The wrapper pattern is employed by many JDBC driver implementations to provide
 24 |  * extensions beyond the traditional JDBC API that are specific to a data source.
 25 |  * Developers may wish to gain access to these resources that are wrapped (the delegates)
 26 |  * as proxy class instances representing the the actual resources.
 27 |  * This class contains a standard mechanism to access these wrapped resources
 28 |  * represented by their proxy, to permit direct access to the resource delegates.
 29 |  *
30 | * 31 | * @author Fabian Groffen, Martin van Dinther 32 | * @version 1.2 33 | */ 34 | public class MonetWrapper implements java.sql.Wrapper { 35 | /** 36 | * Returns true if this either implements the interface argument or 37 | * is directly or indirectly a wrapper for an object that does. 38 | * Returns false otherwise. If this implements the interface then 39 | * return true, else if this is a wrapper then return the result of 40 | * recursively calling isWrapperFor on the wrapped object. 41 | * If this does not implement the interface and is not a wrapper, return 42 | * false. This method should be implemented as a low-cost operation 43 | * compared to unwrap so that callers can use this method to avoid 44 | * expensive unwrap calls that may fail. 45 | * If this method returns true then calling unwrap with the same argument should succeed. 46 | * 47 | * @param iface a Class defining an interface. 48 | * @return true if this implements the interface or directly or indirectly wraps an object that does. 49 | * @throws SQLException if an error occurs while determining whether this is a wrapper 50 | * for an object with the given interface. 51 | * @since 1.6 52 | */ 53 | @Override 54 | public boolean isWrapperFor(final Class iface) throws SQLException { 55 | return iface != null && iface.isAssignableFrom(getClass()); 56 | } 57 | 58 | /** 59 | * Returns an object that implements the given interface to allow 60 | * access to non-standard methods, or standard methods not exposed by the proxy. 61 | * The result may be either the object found to implement the interface 62 | * or a proxy for that object. 63 | * If the receiver implements the interface then the result is the receiver 64 | * or a proxy for the receiver. 65 | * If the receiver is a wrapper and the wrapped object implements the interface 66 | * then the result is the wrapped object or a proxy for the wrapped object. 67 | * Otherwise return the result of calling unwrap recursively on 68 | * the wrapped object or a proxy for that result. 69 | * If the receiver is not a wrapper and does not implement the interface, 70 | * then an SQLException is thrown. 71 | * 72 | * @param iface A Class defining an interface that the result must implement. 73 | * @return an object that implements the interface. May be a proxy for the actual implementing object. 74 | * @throws SQLException If no object found that implements the interface 75 | * @since 1.6 76 | */ 77 | @Override 78 | @SuppressWarnings("unchecked") 79 | public T unwrap(final Class iface) throws SQLException { 80 | if (isWrapperFor(iface)) { 81 | return (T) this; 82 | } 83 | throw new SQLException("Cannot unwrap to interface: " + (iface != null ? iface.getName() : ""), "0A000"); 84 | } 85 | 86 | 87 | /** 88 | * Small helper method that formats the "Method ... not implemented" message 89 | * and creates a new SQLFeatureNotSupportedException object 90 | * whose SQLState is set to "0A000": feature not supported. 91 | * 92 | * @param name the method name 93 | * @return a new created SQLFeatureNotSupportedException object with SQLState 0A000 94 | */ 95 | static final SQLFeatureNotSupportedException newSQLFeatureNotSupportedException(final String name) { 96 | return new SQLFeatureNotSupportedException("Method " + name + " not implemented", "0A000"); 97 | } 98 | 99 | /** 100 | * General utility function to add double quotes around an SQL Identifier 101 | * such as column or table or schema name in SQL queries. 102 | * It also adds escapes for special characters: double quotes and the escape character 103 | * 104 | * FYI: it is made public as it is also called from util/Exporter.java 105 | * 106 | * @param in the string to quote 107 | * @return the double quoted string 108 | */ 109 | public static final String dq(final String in) { 110 | String ret = in; 111 | if (ret.indexOf('\\') >= 0) 112 | // every back slash in input needs to be escaped. 113 | ret = ret.replaceAll("\\\\", "\\\\\\\\"); 114 | if (ret.indexOf('"') >= 0) 115 | // every double quote in input needs to be escaped. 116 | ret = ret.replaceAll("\"", "\\\\\""); 117 | return "\"" + ret + "\""; 118 | } 119 | 120 | /** 121 | * General utility function to add single quotes around string literals as used in SQL queries. 122 | * It also adds escapes for special characters: single quotes and the escape character 123 | * 124 | * FYI: it is made public as it is also called from util/Exporter.java 125 | * 126 | * @param in the string to quote 127 | * @return the single quoted string 128 | */ 129 | public static final String sq(final String in) { 130 | String ret = in; 131 | if (ret.indexOf('\\') >= 0) 132 | // every back slash in input needs to be escaped. 133 | ret = ret.replaceAll("\\\\", "\\\\\\\\"); 134 | if (ret.indexOf('\'') >= 0) 135 | // every single quote in input needs to be escaped. 136 | ret = ret.replaceAll("'", "\\\\'"); 137 | return "'" + ret + "'"; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/jdbc/types/INET.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.jdbc.types; 14 | 15 | import java.net.InetAddress; 16 | import java.sql.SQLDataException; 17 | import java.sql.SQLException; 18 | 19 | /** 20 | * The INET class represents the INET datatype in MonetDB. 21 | * It represents a IPv4 address with a certain mask applied. 22 | * Currently, IPv6 is not supported. 23 | * 24 | * The input format for INET is x.x.x.x/y where x.x.x.x is an IP address 25 | * and y is the number of bits in the netmask. If the /y part is left 26 | * off, then the netmask is 32, and the value represents just a single 27 | * host. On display, the /y portion is suppressed if the netmask is 32. 28 | * 29 | * This class allows to retrieve the value of this INET as InetAddress. 30 | * This is probably meaningful only and only if the netmask is 32. 31 | * The getNetmaskBits() method can be used to retrieve the subnet bits. 32 | */ 33 | public final class INET implements java.sql.SQLData { 34 | private String inet; 35 | 36 | @Override 37 | public String getSQLTypeName() { 38 | return "inet"; 39 | } 40 | 41 | @Override 42 | public void readSQL(final java.sql.SQLInput stream, final String typeName) throws SQLException { 43 | if (!"inet".equals(typeName)) 44 | throw new SQLException("can only use this class with 'inet' type", "M1M05"); 45 | inet = stream.readString(); 46 | } 47 | 48 | @Override 49 | public void writeSQL(final java.sql.SQLOutput stream) throws SQLException { 50 | stream.writeString(inet); 51 | } 52 | 53 | @Override 54 | public String toString() { 55 | return inet; 56 | } 57 | 58 | public void fromString(final String newinet) throws SQLException { 59 | if (newinet == null) { 60 | inet = newinet; 61 | return; 62 | } 63 | 64 | String tinet = newinet; 65 | final int slash = newinet.indexOf('/'); 66 | if (slash != -1) { 67 | final int netmask; 68 | // ok, see if it is a valid netmask 69 | try { 70 | netmask = Integer.parseInt(newinet.substring(slash + 1)); 71 | } catch (NumberFormatException nfe) { 72 | throw new SQLDataException("cannot parse netmask bits: " + 73 | newinet.substring(slash + 1), "22M29"); 74 | } 75 | if (netmask <= 0 || netmask > 32) 76 | throw new SQLDataException("netmask must be >0 and <32", "22M29"); 77 | tinet = newinet.substring(0, slash); 78 | } 79 | // check dotted quad 80 | final String quads[] = tinet.split("\\."); 81 | if (quads.length != 4) 82 | throw new SQLDataException("expected dotted quad (xxx.xxx.xxx.xxx)", "22M29"); 83 | for (int i = 0; i < 4; i++) { 84 | final int quadv; 85 | try { 86 | quadv = Integer.parseInt(quads[i]); 87 | } catch (NumberFormatException nfe) { 88 | throw new SQLDataException("cannot parse number: " + quads[i], "22M29"); 89 | } 90 | if (quadv < 0 || quadv > 255) 91 | throw new SQLDataException("value must be between 0 and 255: " + quads[i], "22M29"); 92 | } 93 | // everything is fine 94 | inet = newinet; 95 | } 96 | 97 | public String getAddress() { 98 | if (inet == null) 99 | return null; 100 | 101 | // inet optionally has a /y part, if y < 32, chop it off 102 | final int slash = inet.indexOf('/'); 103 | if (slash != -1) 104 | return inet.substring(0, slash); 105 | return inet; 106 | } 107 | 108 | public void setAddress(final String newinet) throws Exception { 109 | if (newinet == null) { 110 | inet = newinet; 111 | return; 112 | } 113 | if (newinet.indexOf('/') != -1) 114 | throw new Exception("IPv4 address cannot contain '/' " + 115 | "(use fromString() instead)"); 116 | fromString(newinet); 117 | } 118 | 119 | public int getNetmaskBits() throws SQLException { 120 | if (inet == null) 121 | return 0; 122 | 123 | // if netmask is 32, it is omitted in the output 124 | int slash = inet.indexOf('/'); 125 | if (slash == -1) 126 | return 32; 127 | try { 128 | return Integer.parseInt(inet.substring(slash + 1)); 129 | } catch (NumberFormatException nfe) { 130 | throw new SQLDataException("cannot parse netmask bits: " + 131 | inet.substring(slash + 1), "22M29"); 132 | } 133 | } 134 | 135 | public void setNetmaskBits(final int bits) throws Exception { 136 | String newinet = inet; 137 | if (newinet == null) { 138 | newinet = "0.0.0.0/" + bits; 139 | } else { 140 | final int slash = newinet.indexOf('/'); 141 | if (slash != -1) { 142 | newinet = newinet.substring(0, slash + 1) + bits; 143 | } else { 144 | newinet = newinet + "/" + bits; 145 | } 146 | } 147 | fromString(newinet); 148 | } 149 | 150 | public InetAddress getInetAddress() throws SQLException { 151 | if (inet == null) 152 | return null; 153 | 154 | try { 155 | return InetAddress.getByName(getAddress()); 156 | } catch (java.net.UnknownHostException uhe) { 157 | throw new SQLDataException("could not resolve IP address", "22M29"); 158 | } 159 | } 160 | 161 | public void setInetAddress(final InetAddress iaddr) throws Exception { 162 | if (!(iaddr instanceof java.net.Inet4Address)) 163 | throw new Exception("only IPv4 are supported currently"); 164 | fromString(iaddr.getHostAddress()); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/jdbc/types/URL.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.jdbc.types; 14 | 15 | import java.sql.SQLDataException; 16 | import java.sql.SQLException; 17 | 18 | /** 19 | * The URL class represents the URL datatype in MonetDB. 20 | * It represents an URL, that is, a well-formed string conforming to RFC2396. 21 | */ 22 | public final class URL implements java.sql.SQLData { 23 | /** String url */ 24 | private String url; 25 | 26 | /** 27 | * String getSQLTypeName() 28 | * @return String the type name 29 | */ 30 | @Override 31 | public String getSQLTypeName() { 32 | return "url"; 33 | } 34 | 35 | /** 36 | * void readSQL(final java.sql.SQLInput stream, final String typeName) 37 | * @param stream a java.sql.SQLInput stream 38 | * @param typeName the type name 39 | */ 40 | @Override 41 | public void readSQL(final java.sql.SQLInput stream, final String typeName) throws SQLException { 42 | if (!"url".equals(typeName)) 43 | throw new SQLException("can only use this class with 'url' type", "M1M05"); 44 | url = stream.readString(); 45 | } 46 | 47 | /** 48 | * void writeSQL(final java.sql.SQLOutput stream) 49 | * @param stream a java.sql.SQLOutput stream 50 | */ 51 | @Override 52 | public void writeSQL(final java.sql.SQLOutput stream) throws SQLException { 53 | stream.writeString(url); 54 | } 55 | 56 | /** 57 | * String toString() 58 | * @return String the url string 59 | */ 60 | @Override 61 | public String toString() { 62 | return url; 63 | } 64 | 65 | /** 66 | * void fromString(final String newurl) 67 | * @param newurl the new url string 68 | * @throws Exception when conversion of newurl string to URI or URL object fails 69 | */ 70 | public void fromString(final String newurl) throws Exception { 71 | if (newurl == null) { 72 | url = newurl; 73 | return; 74 | } 75 | 76 | // parse the newurl on validity 77 | // Note: as of Java version 20 java.net.URL(String) constructor is deprecated. 78 | // https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/net/URL.html#%3Cinit%3E(java.lang.String) 79 | new java.net.URI(newurl).toURL(); 80 | // if above doesn't fail (throws an java.net.URISyntaxException | java.net.MalformedURLException), it is fine 81 | url = newurl; 82 | } 83 | 84 | /** 85 | * java.net.URL getURL() 86 | * @return URL an url object 87 | * @throws SQLDataException when conversion of url string to URL object fails 88 | */ 89 | public java.net.URL getURL() throws SQLDataException { 90 | if (url == null) 91 | return null; 92 | 93 | try { 94 | // Note: as of java 20 java.net.URL(String) constructor is deprecated. 95 | // https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/net/URL.html#%3Cinit%3E(java.lang.String) 96 | return new java.net.URI(url).toURL(); 97 | } catch (java.net.URISyntaxException | java.net.MalformedURLException mue) { 98 | throw new SQLDataException("data is not a valid URL: " + mue.getMessage(), "22M30"); 99 | } 100 | } 101 | 102 | /** 103 | * void setURL(final java.net.URL nurl) 104 | * @param nurl a java.net.URL object 105 | */ 106 | public void setURL(final java.net.URL nurl) { 107 | url = nurl.toString(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/mcl/MCLException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.mcl; 14 | 15 | /** 16 | * A general purpose Exception class for MCL related problems. This 17 | * class should be used if no more precise Exception class exists. 18 | */ 19 | @SuppressWarnings("serial") 20 | public final class MCLException extends Exception { 21 | public MCLException(String message) { 22 | super(message); 23 | } 24 | 25 | public MCLException(String message, Exception cause) { 26 | super(message, cause); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/mcl/io/BufferedMCLReader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.mcl.io; 14 | 15 | import java.io.BufferedReader; 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.io.Reader; 19 | import java.nio.charset.Charset; 20 | 21 | /** 22 | * Helper class to read and classify the lines of a query response. 23 | * 24 | * This class wraps and buffers the Reader on which the responses come in. 25 | * Use {@link #getLine()} to get the current line, {@link #getLineType()} 26 | * to get its {@link LineType} and {@link #advance()} to proceed to the 27 | * next line. 28 | * 29 | * Initially, the line type is set to LineType.UNKNOWN and the line 30 | * is set to null. When the end of the result set has been reached the line type 31 | * will remain LineType.PROMPT and the line will again be null. 32 | * 33 | * To start reading the next response, call {@link #resetLineType()}. This 34 | * is usually done automatically by the accompanying {@link BufferedMCLWriter} 35 | * whenever a new request is sent to the server. 36 | * 37 | * @author Fabian Groffen 38 | * @see org.monetdb.mcl.net.MapiSocket 39 | * @see org.monetdb.mcl.io.BufferedMCLWriter 40 | */ 41 | public final class BufferedMCLReader { 42 | 43 | private final BufferedReader inner; 44 | private String current = null; 45 | private LineType lineType = LineType.UNKNOWN; 46 | 47 | /** 48 | * Create a buffering character-input stream that uses a 49 | * default-sized input buffer. 50 | * 51 | * @param in A Reader 52 | */ 53 | public BufferedMCLReader(final Reader in) { 54 | inner = new BufferedReader(in); 55 | } 56 | 57 | /** 58 | * Create a buffering character-input stream that uses a 59 | * default-sized input buffer, from an InputStream. 60 | * 61 | * @param in An InputStream 62 | * @param cs A Charset 63 | */ 64 | public BufferedMCLReader(final InputStream in, final Charset cs) { 65 | this(new java.io.InputStreamReader(in, cs)); 66 | } 67 | 68 | /** 69 | * Proceed to the next line of the response. 70 | * 71 | * Set line type to PROMPT and line to null if the end of the response 72 | * has been reached. 73 | * @throws IOException if exception occurred during reading 74 | */ 75 | public void advance() throws IOException { 76 | if (lineType == LineType.PROMPT) 77 | return; 78 | 79 | current = inner.readLine(); 80 | lineType = LineType.classify(current); 81 | if (lineType == LineType.ERROR && current != null && !current.matches("^![0-9A-Z]{5}!.+")) { 82 | current = "!22000!" + current.substring(1); 83 | } 84 | } 85 | 86 | /** 87 | * Reset the linetype to UNKNOWN. 88 | */ 89 | public void resetLineType() { 90 | lineType = LineType.UNKNOWN; 91 | } 92 | 93 | /** 94 | * Return the current line, or null if we're at the end or before the beginning. 95 | * @return the current line or null 96 | */ 97 | public String getLine() { 98 | return current; 99 | } 100 | 101 | /** 102 | * Return a substring of the current line, or null if we're at the end or before the beginning. 103 | * 104 | * @param start beginIndex 105 | * @return the current line or null 106 | */ 107 | public String getLine(int start) { 108 | String line = getLine(); 109 | if (line != null) 110 | line = line.substring(start); 111 | return line; 112 | } 113 | 114 | /** 115 | * getLineType returns the type of the current line. 116 | * 117 | * @return Linetype representing the kind of line this is, one of the 118 | * following enums: UNKNOWN, HEADER, ERROR, RESULT, 119 | * PROMPT, MORE, FILETRANSFER, SOHEADER, REDIRECT, INFO 120 | */ 121 | public LineType getLineType() { 122 | return lineType; 123 | } 124 | 125 | /** 126 | * Discard the remainder of the response but collect any further error messages. 127 | * 128 | * @return a string containing error messages, or null if there aren't any 129 | * @throws IOException if an IO exception occurs while talking to the server 130 | */ 131 | final public String discardRemainder() throws IOException { 132 | return discard(null); 133 | } 134 | 135 | /** 136 | * Discard the remainder of the response but collect any further error messages. 137 | * 138 | * @param error A string containing earlier error messages to include in the error report. 139 | * @return a string containing error messages, or null if there aren't any 140 | * @throws IOException if an IO exception occurs while talking to the server 141 | */ 142 | final public String discardRemainder(String error) throws IOException { 143 | final StringBuilder sb; 144 | 145 | if (error != null) { 146 | sb = makeErrorBuffer(); 147 | sb.append(error); 148 | } else { 149 | sb = null; 150 | } 151 | return discard(sb); 152 | } 153 | 154 | /** 155 | * Discard the remainder of the response but collect any further error messages. 156 | * 157 | * @param errmsgs An optional StringBuilder object containing earlier error messages to include in the error report. 158 | * @return a string containing error messages, or null if there aren't any 159 | * @throws IOException if an IO exception occurs while talking to the server 160 | * 161 | * TODO(Wouter): should probably not have to be synchronized. 162 | */ 163 | final synchronized String discard(StringBuilder errmsgs) throws IOException { 164 | while (lineType != LineType.PROMPT) { 165 | advance(); 166 | if (getLine() == null) 167 | throw new IOException("Connection to server lost!"); 168 | if (getLineType() == LineType.ERROR) { 169 | if (errmsgs == null) 170 | errmsgs = new StringBuilder(128); 171 | errmsgs.append('\n').append(getLine().substring(1)); 172 | } 173 | } 174 | if (errmsgs == null) 175 | return null; 176 | return errmsgs.toString().trim(); 177 | } 178 | 179 | private final StringBuilder makeErrorBuffer() { 180 | return new StringBuilder(128); 181 | } 182 | 183 | /** 184 | * Close the wrapped Reader. 185 | * @throws IOException if an IO exception occurs while talking to the server 186 | */ 187 | public void close() throws IOException { 188 | inner.close(); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/mcl/io/BufferedMCLWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.mcl.io; 14 | 15 | import java.io.BufferedWriter; 16 | import java.io.IOException; 17 | import java.io.OutputStream; 18 | import java.io.Writer; 19 | import java.nio.charset.Charset; 20 | 21 | /** 22 | * Write text to a character-output stream, buffering characters so as 23 | * to provide a means for efficient writing of single characters, 24 | * arrays, and strings. 25 | * 26 | * In contrast to the BufferedWriter class, this class' newLine() 27 | * method always writes the newline character '\n', regardless the 28 | * platform's own notion of line separator. Apart from that there are 29 | * no differences in the behaviour of this class, compared to its parent 30 | * class, the BufferedWriter. A small convenience is built into this 31 | * class for cooperation with the BufferedMCLReader, via the 32 | * registerReader() method. It causes the reader to be reset upon each 33 | * write performed though this class. This effectuates the MCL protocol 34 | * flow where a write invalidates the state of the read buffers, since 35 | * each write must be answered by the server. That also makes this 36 | * class client-oriented when a reader is registered. 37 | * 38 | * @author Fabian Groffen 39 | * @see org.monetdb.mcl.net.MapiSocket 40 | * @see org.monetdb.mcl.io.BufferedMCLReader 41 | */ 42 | public final class BufferedMCLWriter extends BufferedWriter { 43 | private BufferedMCLReader reader; 44 | 45 | /** 46 | * Create a buffered character-output stream that uses a 47 | * default-sized output buffer. 48 | * 49 | * @param in A Writer 50 | */ 51 | public BufferedMCLWriter(final Writer in) { 52 | super(in); 53 | } 54 | 55 | /** 56 | * Create a buffered character-output stream that uses a 57 | * default-sized output buffer, from an OutputStream. 58 | * 59 | * @param in An OutputStream 60 | * @param cs A Charset 61 | */ 62 | public BufferedMCLWriter(final OutputStream in, final Charset cs) { 63 | super(new java.io.OutputStreamWriter(in, cs)); 64 | } 65 | 66 | /** 67 | * Write a line separator. The line separator string is in this 68 | * class always the single newline character '\n'. 69 | * 70 | * @throws IOException If an I/O error occurs 71 | */ 72 | @Override 73 | public void newLine() throws IOException { 74 | write('\n'); 75 | } 76 | 77 | /** 78 | * Registers the given reader in this writer. A registered reader 79 | * receives a linetype reset when a line is written from this 80 | * writer. 81 | * 82 | * @param r an BufferedMCLReader 83 | */ 84 | public void registerReader(final BufferedMCLReader r) { 85 | reader = r; 86 | } 87 | 88 | /** 89 | * Write a single line, terminated with a line separator, and flush 90 | * the stream. This is a shorthand method for a call to write() 91 | * and flush(). 92 | * 93 | * @param line The line to write 94 | * @throws IOException If an I/O error occurs 95 | */ 96 | public void writeLine(final String line) throws IOException { 97 | write(line); 98 | flush(); 99 | 100 | // reset reader state, last line isn't valid any more now 101 | if (reader != null) 102 | reader.resetLineType(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/mcl/io/LineType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.mcl.io; 14 | 15 | /** 16 | * Enumeration of the various message types used in the MAPI protocol. 17 | */ 18 | public enum LineType { 19 | /** "there is currently no line", or the type is unknown is represented by UNKNOWN */ 20 | UNKNOWN(null), 21 | 22 | /** a line starting with ! indicates ERROR */ 23 | ERROR(new byte[] { '!' }), 24 | 25 | /** a line starting with % indicates HEADER */ 26 | HEADER(new byte[] { '%' }), 27 | 28 | /** a line starting with [ or = indicates RESULT */ 29 | RESULT(new byte[] { '[' }), 30 | 31 | /** a line which matches the pattern of prompt1 is a PROMPT */ 32 | PROMPT(new byte[] { 1, 1 }), 33 | 34 | /** a line which matches the pattern of prompt2 is a MORE */ 35 | MORE(new byte[] { 1, 2 }), 36 | 37 | /** a line which matches the pattern of prompt3 is a FILETRANSFER */ 38 | FILETRANSFER(new byte[] { 1, 3 }), 39 | 40 | /** a line starting with & indicates the start of a header block */ 41 | SOHEADER(new byte[] { '&' }), 42 | 43 | /** a line starting with ^ indicates REDIRECT */ 44 | REDIRECT(new byte[] { '^' }), 45 | 46 | /** a line starting with # indicates INFO */ 47 | INFO(new byte[] { '#' }); 48 | 49 | private final byte[] bytes; 50 | 51 | LineType(byte[] bytes) { 52 | this.bytes = bytes; 53 | } 54 | 55 | public final byte[] bytes() { 56 | return this.bytes; 57 | } 58 | 59 | /** 60 | * Look at a mapi message and decide the LineType 61 | * 62 | * @param line the line containing the coded LineType 63 | * @return LineType the type of line 64 | */ 65 | public static final LineType classify(String line) { 66 | if (line != null) { 67 | if (line.length() > 1) { 68 | return classify(line.charAt(0), line.charAt(1)); 69 | } else if (line.length() == 1) { 70 | return classify(line.charAt(0), 0); 71 | } 72 | } 73 | return UNKNOWN; 74 | } 75 | 76 | /** 77 | * Look at a mapi message and decide the LineType 78 | * 79 | * @param line the line containing the coded LineType 80 | * @return LineType the type of line 81 | */ 82 | public static final LineType classify(byte[] line) { 83 | if (line != null) { 84 | if (line.length > 1) { 85 | return classify(line[0], line[1]); 86 | } else if (line.length == 1) { 87 | return classify(line[0], 0); 88 | } 89 | } 90 | return UNKNOWN; 91 | } 92 | 93 | /** 94 | * utility method to decide the LineType 95 | * 96 | * @param ch0 the first byte as int 97 | * @param ch1 the second byte as int 98 | * @return LineType the type of line 99 | */ 100 | private static final LineType classify(int ch0, int ch1) { 101 | switch (ch0) { 102 | case '!': 103 | return ERROR; 104 | case '%': 105 | return HEADER; 106 | case '[': // tuple result 107 | case '=': // single value result or result of PLAN/EXPLAIN/TRACE queries 108 | return RESULT; 109 | case '&': 110 | return SOHEADER; 111 | case '^': 112 | return REDIRECT; 113 | case '#': 114 | return INFO; 115 | case 1: 116 | // prompts, see below 117 | break; 118 | default: 119 | return UNKNOWN; 120 | } 121 | 122 | switch (ch1) { 123 | case 1: 124 | return PROMPT; 125 | case 2: 126 | return MORE; 127 | case 3: 128 | return FILETRANSFER; 129 | default: 130 | return UNKNOWN; 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/mcl/net/ClientInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.mcl.net; 14 | 15 | import org.monetdb.jdbc.MonetDriver; 16 | 17 | import java.io.File; 18 | import java.lang.management.ManagementFactory; 19 | import java.lang.management.RuntimeMXBean; 20 | import java.net.InetAddress; 21 | import java.net.UnknownHostException; 22 | import java.sql.ClientInfoStatus; 23 | import java.sql.SQLClientInfoException; 24 | import java.sql.SQLException; 25 | import java.sql.SQLWarning; 26 | import java.util.HashMap; 27 | import java.util.Map; 28 | import java.util.Properties; 29 | import java.util.Set; 30 | 31 | /** 32 | * Manage ClientInfo properties to track, and help generating a 33 | * {@link SQLClientInfoException} if there is a failure 34 | */ 35 | public final class ClientInfo { 36 | public static final String defaultHostname = findHostname(); 37 | 38 | public static final String defaultClientLibrary = findClientLibrary(); 39 | 40 | public static final String defaultApplicationName = findApplicationName(); 41 | 42 | public static final String defaultPid = findPid(); 43 | 44 | private final Properties props; 45 | private HashMap problems = null; 46 | 47 | public ClientInfo() { 48 | props = new Properties(); 49 | } 50 | 51 | public void setDefaults() { 52 | props.setProperty("ClientHostname", defaultHostname); 53 | props.setProperty("ClientLibrary", defaultClientLibrary); 54 | props.setProperty("ClientPid", defaultPid); 55 | props.setProperty("ApplicationName", defaultApplicationName); 56 | props.setProperty("ClientRemark", ""); 57 | } 58 | 59 | private static String findHostname() { 60 | try { 61 | return InetAddress.getLocalHost().getHostName(); 62 | } catch (UnknownHostException e) { 63 | return ""; 64 | } 65 | } 66 | 67 | private static String findApplicationName() { 68 | String appName = ""; 69 | try { 70 | String prop = System.getProperty("sun.java.command"); 71 | if (prop != null) { 72 | // we want only the command, and not the arguments 73 | prop = prop.split("\\s", 2)[0]; 74 | // keep only the basename5 75 | int idx = prop.lastIndexOf(File.separatorChar); 76 | if (idx >= 0) 77 | prop = prop.substring(idx + 1); 78 | appName = prop; 79 | } 80 | } catch (SecurityException e) { 81 | // ignore 82 | } 83 | 84 | return appName; 85 | } 86 | 87 | private static String findPid() { 88 | try { 89 | RuntimeMXBean mxbean = ManagementFactory.getRuntimeMXBean(); 90 | String pidAtHostname = mxbean.getName(); 91 | return pidAtHostname.split("@", 2)[0]; 92 | } catch (RuntimeException e) { 93 | return ""; 94 | } 95 | } 96 | 97 | private static String findClientLibrary() { 98 | return "monetdb-java " + MonetDriver.getDriverVersion(); 99 | } 100 | 101 | public String format() { 102 | StringBuilder builder = new StringBuilder(200); 103 | for (String name : props.stringPropertyNames()) { 104 | String value = props.getProperty(name); 105 | builder.append(name); 106 | builder.append('='); 107 | builder.append(value); 108 | builder.append('\n'); 109 | } 110 | return builder.toString(); 111 | } 112 | 113 | public Properties get() { 114 | return props; 115 | } 116 | 117 | public HashMap getProblems() { 118 | return problems; 119 | } 120 | 121 | public void set(String name, String value, Set known) throws SQLClientInfoException { 122 | if (value == null) 123 | value = ""; 124 | 125 | if (known != null && !known.contains(name)) { 126 | addProblem(name, ClientInfoStatus.REASON_UNKNOWN_PROPERTY); 127 | } else if (value.contains("\n")) { 128 | addProblem(name, ClientInfoStatus.REASON_VALUE_INVALID); 129 | throw new SQLClientInfoException("Invalid value for Client Info property '" + name + "'", "01M07", problems); 130 | } else { 131 | props.setProperty(name, value); 132 | } 133 | } 134 | 135 | public void set(String name, String value) throws SQLClientInfoException { 136 | set(name, value, null); 137 | } 138 | 139 | private void addProblem(String name, ClientInfoStatus status) { 140 | if (problems == null) 141 | problems = new HashMap<>(); 142 | ClientInfoStatus old = problems.get(name); 143 | if (old == null || status.compareTo(old) > 0) 144 | problems.put(name, status); 145 | } 146 | 147 | public SQLClientInfoException wrapException(SQLException e) { 148 | return new SQLClientInfoException(problems, e); 149 | } 150 | 151 | public SQLWarning warnings() { 152 | SQLWarning ret = null; 153 | if (problems == null) 154 | return null; 155 | for (Map.Entry entry: problems.entrySet()) { 156 | if (!entry.getValue().equals(ClientInfoStatus.REASON_UNKNOWN_PROPERTY)) 157 | continue; 158 | SQLWarning warning = new SQLWarning("unknown client info property: " + entry.getKey(), "01M07"); 159 | if (ret == null) 160 | ret = warning; 161 | else 162 | ret.setNextWarning(warning); 163 | } 164 | return ret; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/mcl/net/HandshakeOption.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.mcl.net; 14 | 15 | public abstract class HandshakeOption { 16 | protected final int level; 17 | protected final String handshakeField; 18 | boolean sent = false; 19 | T desiredValue; 20 | 21 | protected HandshakeOption(int level, String handshakeField, T desiredValue) { 22 | if (desiredValue == null) { 23 | throw new IllegalArgumentException("initial value must not be null"); 24 | } 25 | this.level = level; 26 | this.handshakeField = handshakeField; 27 | this.desiredValue = desiredValue; 28 | } 29 | 30 | public void set(T newValue) { 31 | if (newValue == null) { 32 | throw new IllegalArgumentException("new value must not be null"); 33 | } 34 | desiredValue = newValue; 35 | } 36 | 37 | public T get() { 38 | return desiredValue; 39 | } 40 | 41 | public int getLevel() { 42 | return level; 43 | } 44 | 45 | public String getFieldName() { 46 | return handshakeField; 47 | } 48 | 49 | public boolean isSent() { 50 | return sent; 51 | } 52 | 53 | public void setSent(boolean b) { 54 | sent = b; 55 | } 56 | 57 | public boolean mustSend(T currentValue) { 58 | if (sent) 59 | return false; 60 | if (currentValue.equals(desiredValue)) 61 | return false; 62 | return true; 63 | } 64 | 65 | abstract long numericValue(); 66 | 67 | protected static class BooleanOption extends HandshakeOption { 68 | protected BooleanOption(int level, String name, Boolean initialValue) { 69 | super(level, name, initialValue); 70 | } 71 | 72 | @Override 73 | long numericValue() { 74 | return desiredValue ? 1 : 0; 75 | } 76 | } 77 | 78 | public final static class AutoCommit extends BooleanOption { 79 | public AutoCommit(boolean autoCommit) { 80 | super(1, "auto_commit", autoCommit); 81 | } 82 | } 83 | 84 | public final static class ReplySize extends HandshakeOption { 85 | public ReplySize(int size) { 86 | super(2, "reply_size", size); 87 | } 88 | 89 | @Override 90 | long numericValue() { 91 | return desiredValue; 92 | } 93 | } 94 | 95 | public final static class SizeHeader extends BooleanOption { 96 | public SizeHeader(boolean sendHeader) { 97 | super(3, "size_header", sendHeader); 98 | set(sendHeader); 99 | } 100 | } 101 | 102 | public final static class TimeZone extends HandshakeOption { 103 | public TimeZone(int offset) { 104 | super(5, "time_zone", offset); 105 | } 106 | 107 | @Override 108 | long numericValue() { 109 | return desiredValue; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/mcl/net/MonetUrlParser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.mcl.net; 14 | 15 | import java.io.UnsupportedEncodingException; 16 | import java.net.URI; 17 | import java.net.URISyntaxException; 18 | import java.net.URLDecoder; 19 | import java.net.URLEncoder; 20 | 21 | /** 22 | * Helper class to keep the URL parsing code separate from the rest of 23 | * the {@link Target} class. 24 | */ 25 | public final class MonetUrlParser { 26 | private final Target target; 27 | private final String urlText; 28 | private final URI url; 29 | 30 | private MonetUrlParser(Target target, String url) throws URISyntaxException { 31 | this.target = target; 32 | this.urlText = url; 33 | // we want to accept monetdb:// but the Java URI parser rejects that. 34 | switch (url) { 35 | case "monetdb:-": 36 | case "monetdbs:-": 37 | throw new URISyntaxException(url, "invalid MonetDB URL"); 38 | case "monetdb://": 39 | case "monetdbs://": 40 | url += "-"; 41 | break; 42 | } 43 | this.url = new URI(url); 44 | } 45 | 46 | public static void parse(Target target, String url) throws URISyntaxException, ValidationError { 47 | if (url.equals("monetdb://")) { 48 | // deal with peculiarity of Java's URI parser 49 | url = "monetdb:///"; 50 | } 51 | 52 | target.barrier(); 53 | if (url.startsWith("mapi:")) { 54 | try { 55 | MonetUrlParser parser = new MonetUrlParser(target, url.substring(5)); 56 | parser.parseClassic(); 57 | } catch (URISyntaxException e) { 58 | URISyntaxException exc = new URISyntaxException(e.getInput(), e.getReason(), -1); 59 | exc.setStackTrace(e.getStackTrace()); 60 | throw exc; 61 | } 62 | } else { 63 | MonetUrlParser parser = new MonetUrlParser(target, url); 64 | parser.parseModern(); 65 | } 66 | target.barrier(); 67 | } 68 | 69 | public static String percentDecode(String context, String text) throws URISyntaxException { 70 | try { 71 | return URLDecoder.decode(text, "UTF-8"); 72 | } catch (UnsupportedEncodingException e) { 73 | throw new IllegalStateException("should be unreachable: UTF-8 unknown??", e); 74 | } catch (IllegalArgumentException e) { 75 | throw new URISyntaxException(text, context + ": invalid percent escape"); 76 | } 77 | } 78 | 79 | public static String percentEncode(String text) { 80 | try { 81 | return URLEncoder.encode(text, "UTF-8"); 82 | } catch (UnsupportedEncodingException e) { 83 | throw new RuntimeException(e); 84 | } 85 | } 86 | 87 | @SuppressWarnings("fallthrough") 88 | private void parseModern() throws URISyntaxException, ValidationError { 89 | clearBasic(); 90 | 91 | String scheme = url.getScheme(); 92 | if (scheme == null) 93 | throw new URISyntaxException(urlText, "URL scheme must be monetdb:// or monetdbs://"); 94 | switch (scheme) { 95 | case "monetdb": 96 | target.setTls(false); 97 | break; 98 | case "monetdbs": 99 | target.setTls(true); 100 | break; 101 | default: 102 | throw new URISyntaxException(urlText, "URL scheme must be monetdb:// or monetdbs://"); 103 | } 104 | 105 | // The built-in getHost and getPort methods do strange things 106 | // in edge cases such as percent-encoded host names and 107 | // invalid port numbers 108 | String authority = url.getAuthority(); 109 | String host; 110 | String remainder; 111 | int pos; 112 | if (authority == null) { 113 | if (!url.getRawSchemeSpecificPart().startsWith("//")) { 114 | throw new URISyntaxException(urlText, "expected //"); 115 | } 116 | host = ""; 117 | remainder = ""; 118 | } else if (authority.equals("-")) { 119 | host = ""; 120 | remainder = ""; 121 | } else { 122 | if (authority.startsWith("[")) { 123 | // IPv6 124 | pos = authority.indexOf(']'); 125 | if (pos < 0) 126 | throw new URISyntaxException(urlText, "unmatched '['"); 127 | host = authority.substring(1, pos); 128 | remainder = authority.substring(pos + 1); 129 | } else if ((pos = authority.indexOf(':')) >= 0) { 130 | host = authority.substring(0, pos); 131 | remainder = authority.substring(pos); 132 | } else { 133 | host = authority; 134 | remainder = ""; 135 | } 136 | } 137 | host = Target.unpackHost(host); 138 | target.setHost(host); 139 | 140 | if (remainder.isEmpty()) { 141 | // do nothing 142 | } else if (remainder.startsWith(":")) { 143 | String portStr = remainder.substring(1); 144 | try { 145 | int port = Integer.parseInt(portStr); 146 | if (port <= 0 || port > 65535) 147 | portStr = null; 148 | } catch (NumberFormatException e) { 149 | portStr = null; 150 | } 151 | if (portStr == null) 152 | throw new ValidationError(urlText, "invalid port number"); 153 | target.setString(Parameter.PORT, portStr); 154 | } 155 | 156 | String path = url.getRawPath(); 157 | String[] parts = path.split("/", 4); 158 | // <0: empty before leading slash> / <1: database> / <2: tableschema> / <3: table> / <4: should not exist> 159 | switch (parts.length) { 160 | case 4: 161 | target.setString(Parameter.TABLE, percentDecode(Parameter.TABLE.name, parts[3])); 162 | // fallthrough 163 | case 3: 164 | target.setString(Parameter.TABLESCHEMA, percentDecode(Parameter.TABLESCHEMA.name, parts[2])); 165 | // fallthrough 166 | case 2: 167 | target.setString(Parameter.DATABASE, percentDecode(Parameter.DATABASE.name, parts[1])); 168 | case 1: 169 | case 0: 170 | // fallthrough 171 | break; 172 | } 173 | 174 | final String query = url.getRawQuery(); 175 | if (query != null) { 176 | final String[] args = query.split("&"); 177 | for (int i = 0; i < args.length; i++) { 178 | pos = args[i].indexOf('='); 179 | if (pos <= 0) { 180 | throw new URISyntaxException(args[i], "invalid key=value pair"); 181 | } 182 | String key = args[i].substring(0, pos); 183 | key = percentDecode(key, key); 184 | Parameter parm = Parameter.forName(key); 185 | if (parm != null && parm.isCore) 186 | throw new URISyntaxException(key, key + "= is not allowed as a query parameter"); 187 | 188 | String value = args[i].substring(pos + 1); 189 | target.setString(key, percentDecode(key, value)); 190 | } 191 | } 192 | } 193 | 194 | private void parseClassic() throws URISyntaxException, ValidationError { 195 | if (!url.getRawSchemeSpecificPart().startsWith("//")) { 196 | throw new URISyntaxException(urlText, "expected //"); 197 | } 198 | 199 | String scheme = url.getScheme(); 200 | if (scheme == null) 201 | scheme = ""; 202 | switch (scheme) { 203 | case "monetdb": 204 | parseClassicAuthorityAndPath(); 205 | break; 206 | case "merovingian": 207 | String authority = url.getRawAuthority(); 208 | // authority must be "proxy" ignore authority and path 209 | boolean valid = urlText.startsWith("merovingian://proxy?") || urlText.equals("merovingian://proxy"); 210 | if (!valid) 211 | throw new URISyntaxException(urlText, "with mapi:merovingian:, only //proxy is supported"); 212 | break; 213 | default: 214 | throw new URISyntaxException(urlText, "URL scheme must be mapi:monetdb:// or mapi:merovingian://"); 215 | } 216 | 217 | final String query = url.getRawQuery(); 218 | if (query != null) { 219 | final String[] args = query.split("&"); 220 | for (int i = 0; i < args.length; i++) { 221 | String arg = args[i]; 222 | if (arg.startsWith("language=")) { 223 | String language = arg.substring(9); 224 | target.setString(Parameter.LANGUAGE, language); 225 | } else if (arg.startsWith("database=")) { 226 | String database = arg.substring(9); 227 | target.setString(Parameter.DATABASE, database); 228 | } else { 229 | // ignore 230 | } 231 | } 232 | } 233 | } 234 | 235 | private void parseClassicAuthorityAndPath() throws URISyntaxException, ValidationError { 236 | clearBasic(); 237 | String authority = url.getRawAuthority(); 238 | String host; 239 | String portStr; 240 | int pos; 241 | if (authority == null) { 242 | host = ""; 243 | portStr = ""; 244 | } else if (authority.indexOf('@') >= 0) { 245 | throw new URISyntaxException(urlText, "user@host syntax is not allowed"); 246 | } else if ((pos = authority.indexOf(':')) >= 0) { 247 | host = authority.substring(0, pos); 248 | portStr = authority.substring(pos + 1); 249 | } else { 250 | host = authority; 251 | portStr = ""; 252 | } 253 | 254 | if (!portStr.isEmpty()) { 255 | int port; 256 | try { 257 | port = Integer.parseInt(portStr); 258 | } catch (NumberFormatException e) { 259 | port = -1; 260 | } 261 | if (port <= 0) { 262 | throw new ValidationError(urlText, "invalid port number"); 263 | } 264 | target.setString(Parameter.PORT, portStr); 265 | } 266 | 267 | String path = url.getRawPath(); 268 | if (host.isEmpty() && portStr.isEmpty()) { 269 | // socket 270 | target.clear(Parameter.HOST); 271 | target.setString(Parameter.SOCK, path != null ? path : ""); 272 | } else { 273 | // tcp 274 | target.clear(Parameter.SOCK); 275 | target.setString(Parameter.HOST, host); 276 | if (path == null || path.isEmpty()) { 277 | // do nothing 278 | } else if (!path.startsWith("/")) { 279 | throw new URISyntaxException(urlText, "expect path to start with /"); 280 | } else { 281 | String database = path.substring(1); 282 | target.setString(Parameter.DATABASE, database); 283 | } 284 | } 285 | } 286 | 287 | private void clearBasic() { 288 | target.clear(Parameter.TLS); 289 | target.clear(Parameter.HOST); 290 | target.clear(Parameter.PORT); 291 | target.clear(Parameter.DATABASE); 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/mcl/net/Parameter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.mcl.net; 14 | 15 | import java.sql.DriverPropertyInfo; 16 | import java.util.ArrayList; 17 | import java.util.Calendar; 18 | import java.util.Properties; 19 | 20 | /** 21 | * Enumerates things that can be configured on a connection to MonetDB. 22 | */ 23 | public enum Parameter { 24 | // String name, ParameterType type, Object defaultValue, String description, boolean isCore 25 | TLS("tls", ParameterType.Bool, false, "secure the connection using TLS", true, true), 26 | HOST("host", ParameterType.Str, "", "IP number, domain name or one of the special values `localhost` and `localhost.`", true), 27 | PORT("port", ParameterType.Int, -1, "Port to connect to, 1..65535 or -1 for 'not set'", true), 28 | DATABASE("database", ParameterType.Str, "", "name of database to connect to", true), 29 | TABLESCHEMA("tableschema", ParameterType.Str, "", "only used for REMOTE TABLE, otherwise unused", true), 30 | TABLE("table", ParameterType.Str, "", "only used for REMOTE TABLE, otherwise unused", true), 31 | SOCK("sock", ParameterType.Path, "", "path to Unix domain socket to connect to", false), 32 | SOCKDIR("sockdir", ParameterType.Path, "/tmp", "Directory for implicit Unix domain sockets (.s.monetdb.PORT)", false), 33 | CERT("cert", ParameterType.Path, "", "path to TLS certificate to authenticate server with", false, true), 34 | CERTHASH("certhash", ParameterType.Str, "", "hash of server TLS certificate must start with these hex digits; overrides cert", false, true), 35 | CLIENTKEY("clientkey", ParameterType.Path, "", "path to TLS key (+certs) to authenticate with as client", false, true), 36 | CLIENTCERT("clientcert", ParameterType.Path, "", "path to TLS certs for 'clientkey', if not included there", false, true), 37 | USER("user", ParameterType.Str, "", "user name to authenticate as", false), 38 | PASSWORD("password", ParameterType.Str, "", "password to authenticate with", false), 39 | LANGUAGE("language", ParameterType.Str, "sql", "for example, \"sql\", \"mal\", \"msql\", \"profiler\"", false), 40 | AUTOCOMMIT("autocommit", ParameterType.Bool, true, "initial value of autocommit", false), 41 | SCHEMA("schema", ParameterType.Str, "", "initial schema", false), 42 | TIMEZONE("timezone", ParameterType.Int, null, "client time zone as minutes east of UTC", false), 43 | BINARY("binary", ParameterType.Str, "on", "whether to use binary result set format (number or bool)", false), 44 | REPLYSIZE("replysize", ParameterType.Int, 250, "rows beyond this limit are retrieved on demand, <1 means unlimited", false), 45 | FETCHSIZE("fetchsize", ParameterType.Int, null, "alias for replysize, specific to jdbc", false), 46 | HASH("hash", ParameterType.Str, "", "specific to jdbc", false), 47 | DEBUG("debug", ParameterType.Bool, false, "enable tracing of socket communication for debugging", false), 48 | LOGFILE("logfile", ParameterType.Str, "", "when debug is enabled its output will be written to this logfile", false), 49 | SO_TIMEOUT("so_timeout", ParameterType.Int, 0, "abort if network I/O does not complete in this many milliseconds, 0 means no timeout", false), 50 | CLOB_AS_VARCHAR("treat_clob_as_varchar", ParameterType.Bool, true, "map CLOB/TEXT data to type VARCHAR instead of type CLOB", false), 51 | BLOB_AS_BINARY("treat_blob_as_binary", ParameterType.Bool, true, "map BLOB data to type BINARY instead of type BLOB", false), 52 | 53 | CLIENT_INFO("client_info", ParameterType.Bool, true, "whether to send ClientInfo when connecting", false), 54 | CLIENT_APPLICATION("client_application", ParameterType.Str, "", "application name to send in ClientInfo", false), 55 | CLIENT_REMARK("client_remark", ParameterType.Str, "", "any client remark to send in ClientInfo", false), 56 | ; 57 | 58 | public final String name; 59 | public final ParameterType type; 60 | private final Object defaultValue; 61 | public final String description; 62 | public final boolean isCore; 63 | public final boolean isTlsRelated; 64 | 65 | Parameter(String name, ParameterType type, Object defaultValue, String description, boolean isCore, boolean isTlsRelated) { 66 | this.name = name; 67 | this.type = type; 68 | this.defaultValue = defaultValue; 69 | this.description = description; 70 | this.isCore = isCore; 71 | this.isTlsRelated = isTlsRelated; 72 | } 73 | 74 | Parameter(String name, ParameterType type, Object defaultValue, String description, boolean isCore) { 75 | this(name, type, defaultValue, description, isCore, false); 76 | } 77 | 78 | public static Parameter forName(String name) { 79 | switch (name) { 80 | case "tls": 81 | return TLS; 82 | case "host": 83 | return HOST; 84 | case "port": 85 | return PORT; 86 | case "database": 87 | return DATABASE; 88 | case "tableschema": 89 | return TABLESCHEMA; 90 | case "table": 91 | return TABLE; 92 | case "sock": 93 | return SOCK; 94 | case "sockdir": 95 | return SOCKDIR; 96 | case "cert": 97 | return CERT; 98 | case "certhash": 99 | return CERTHASH; 100 | case "clientkey": 101 | return CLIENTKEY; 102 | case "clientcert": 103 | return CLIENTCERT; 104 | case "user": 105 | return USER; 106 | case "password": 107 | return PASSWORD; 108 | case "language": 109 | return LANGUAGE; 110 | case "autocommit": 111 | return AUTOCOMMIT; 112 | case "schema": 113 | return SCHEMA; 114 | case "timezone": 115 | return TIMEZONE; 116 | case "binary": 117 | return BINARY; 118 | case "replysize": 119 | return REPLYSIZE; 120 | case "fetchsize": 121 | return FETCHSIZE; 122 | case "hash": 123 | return HASH; 124 | case "debug": 125 | return DEBUG; 126 | case "logfile": 127 | return LOGFILE; 128 | case "so_timeout": 129 | return SO_TIMEOUT; 130 | case "treat_clob_as_varchar": 131 | return CLOB_AS_VARCHAR; 132 | case "treat_blob_as_binary": 133 | return BLOB_AS_BINARY; 134 | case "client_info": 135 | return CLIENT_INFO; 136 | case "client_application": 137 | return CLIENT_APPLICATION; 138 | case "client_remark": 139 | return CLIENT_REMARK; 140 | default: 141 | return null; 142 | } 143 | } 144 | 145 | /** 146 | * Determine if a given setting can safely be ignored. 147 | * The ground rule is that if we encounter an unknown setting 148 | * without an underscore in the name, it is an error. If it has 149 | * an underscore in its name, it can be ignored. 150 | * 151 | * @param name the name of the setting to check 152 | * @return true if it can safely be ignored 153 | */ 154 | public static boolean isIgnored(String name) { 155 | if (Parameter.forName(name) != null) 156 | return false; 157 | return name.contains("_"); 158 | } 159 | 160 | /** 161 | * Return a default value for the given setting, as an Object of the appropriate type. 162 | * Note that the value returned for TIMEZONE may change if the system time zone 163 | * is changed or if Daylight Saving Time starts or ends. 164 | * 165 | * @return default value for the given setting, as an Object of the appropriate type 166 | */ 167 | public Object getDefault() { 168 | switch (this) { 169 | case TIMEZONE: 170 | Calendar cal = Calendar.getInstance(); 171 | int offsetMillis = cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET); 172 | int offsetMinutes = offsetMillis / 60000; 173 | return offsetMinutes; 174 | default: 175 | return defaultValue; 176 | } 177 | } 178 | 179 | /** 180 | * Determine if this Parameter is only relevant when TlS is enabled. 181 | * 182 | * Such parameters need not be shown to the user unless the URL starts with monetdbs://. 183 | * 184 | * @return true if this Parameter is only relevant when TLS is enabled 185 | */ 186 | public boolean isTlsOnly() { 187 | switch (this) { 188 | case CERT: 189 | case CERTHASH: 190 | case CLIENTCERT: 191 | case CLIENTKEY: 192 | return true; 193 | default: 194 | return false; 195 | } 196 | } 197 | 198 | /** 199 | * Gets information about the possible properties for this driver. 200 | * 201 | * The getPropertyInfo method is intended to allow a generic GUI tool to 202 | * discover what properties it should prompt a human for in order to get 203 | * enough information to connect to a database. Note that depending on the 204 | * values the human has supplied so far, additional values may become 205 | * necessary, so it may be necessary to iterate through several calls to the 206 | * getPropertyInfo method. 207 | * 208 | * Note: This method is called from jdbc.MonetDriver.getPropertyInfo() 209 | * 210 | * @param info a proposed list of tag/value pairs that will be sent on 211 | * connect open 212 | * @param includeTls include TLS related properties. 213 | * @return an array of DriverPropertyInfo objects describing possible 214 | * properties. This array may be an empty array if no properties 215 | * are required. 216 | */ 217 | public static DriverPropertyInfo[] getPropertyInfo(final Properties info, boolean includeTls) { 218 | final String[] booleanChoices = new String[] { "true", "false" }; 219 | final ArrayList mandatory = new ArrayList<>(Parameter.values().length); 220 | final ArrayList optional = new ArrayList<>(Parameter.values().length); 221 | 222 | for (Parameter parm: Parameter.values()) { 223 | if (!includeTls && parm.isTlsRelated) 224 | continue; 225 | switch (parm) { 226 | case TABLESCHEMA: 227 | case TABLE: 228 | case SOCK: 229 | case SOCKDIR: 230 | case BINARY: 231 | // hide, not supported in Java 232 | continue; 233 | case FETCHSIZE: 234 | // hide, alias of REPLYSIZE 235 | continue; 236 | case LANGUAGE: 237 | case HASH: 238 | // hide, we don't want users to mess with this 239 | continue; 240 | default: 241 | break; 242 | } 243 | if (parm == Parameter.FETCHSIZE) // alias of REPLYSIZE 244 | continue; 245 | String value = info == null ? null : info.getProperty(parm.name); 246 | if (value == null) { 247 | Object defaultValue = parm.getDefault(); 248 | if (defaultValue != null) 249 | value = defaultValue.toString(); 250 | } 251 | DriverPropertyInfo propInfo = new DriverPropertyInfo(parm.name, value); 252 | propInfo.description = parm.description; 253 | if (parm.type == ParameterType.Bool) 254 | propInfo.choices = booleanChoices; 255 | propInfo.required = (parm == Parameter.USER || parm == Parameter.PASSWORD); 256 | if (propInfo.required) 257 | mandatory.add(propInfo); 258 | else 259 | optional.add(propInfo); 260 | } 261 | 262 | final DriverPropertyInfo[] result = new DriverPropertyInfo[mandatory.size() + optional.size()]; 263 | int i = 0; 264 | for (DriverPropertyInfo propInfo: mandatory) 265 | result[i++] = propInfo; 266 | for (DriverPropertyInfo propInfo: optional) 267 | result[i++] = propInfo; 268 | 269 | return result; 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/mcl/net/ParameterType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.mcl.net; 14 | 15 | /** 16 | * Enumeration of the types a {@link Parameter} may have. 17 | */ 18 | public enum ParameterType { 19 | /** 20 | * The Parameter is an arbitrary string 21 | */ 22 | Str, 23 | /** The Parameter can be interpreted as an {@link Integer} */ 24 | Int, 25 | /** The Parameter is a {@link Boolean} and can be 26 | * written "true", "false", "on", "off", "yes" or "no". 27 | * Uppercase letters are also accepted 28 | */ 29 | Bool, 30 | /** 31 | * Functionally the same as {@link ParameterType#Str} but 32 | * indicates the value is to be interpreted as a path on the 33 | * client's file system. 34 | */ 35 | Path; 36 | 37 | /** 38 | * Convert a string to a boolean, accepting true/false/yes/no/on/off. 39 | * 40 | * Uppercase is also accepted. 41 | * 42 | * @param value text to be parsed 43 | * @return boolean interpretation of the text 44 | */ 45 | public static boolean parseBool(String value) { 46 | boolean lowered = false; 47 | String original = value; 48 | while (true) { 49 | switch (value) { 50 | case "true": 51 | case "yes": 52 | case "on": 53 | return true; 54 | case "false": 55 | case "no": 56 | case "off": 57 | return false; 58 | default: 59 | if (!lowered) { 60 | value = value.toLowerCase(); 61 | lowered = true; 62 | continue; 63 | } 64 | throw new IllegalArgumentException("invalid boolean value: " + original); 65 | } 66 | } 67 | } 68 | 69 | /** 70 | * Convert text into an Object of the appropriate type 71 | * 72 | * @param name name of the setting for use in error messages 73 | * @param value text to be converted 74 | * @return Object representation of the text 75 | * @throws ValidationError if the text cannot be converted 76 | */ 77 | public Object parse(String name, String value) throws ValidationError { 78 | if (value == null) 79 | return null; 80 | 81 | try { 82 | switch (this) { 83 | case Bool: 84 | return parseBool(value); 85 | case Int: 86 | return Integer.parseInt(value); 87 | case Str: 88 | case Path: 89 | return value; 90 | default: 91 | throw new IllegalStateException("unreachable"); 92 | } 93 | } catch (IllegalArgumentException e) { 94 | String message = e.toString(); 95 | throw new ValidationError(name, message); 96 | } 97 | } 98 | 99 | /** 100 | * Represent the object as a string. 101 | * 102 | * @param value, must be of the appropriate type 103 | * @return textual representation 104 | */ 105 | public String format(Object value) { 106 | switch (this) { 107 | case Bool: 108 | return (Boolean) value ? "true" : "false"; 109 | case Int: 110 | return Integer.toString((Integer) value); 111 | case Str: 112 | case Path: 113 | return (String) value; 114 | default: 115 | throw new IllegalStateException("unreachable"); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/mcl/net/SecureSocket.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.mcl.net; 14 | 15 | import javax.net.ssl.*; 16 | import java.io.FileInputStream; 17 | import java.io.IOException; 18 | import java.lang.reflect.InvocationTargetException; 19 | import java.lang.reflect.Method; 20 | import java.net.Socket; 21 | import java.security.*; 22 | import java.security.cert.CertificateException; 23 | import java.security.cert.CertificateFactory; 24 | import java.security.cert.X509Certificate; 25 | import java.util.Collections; 26 | 27 | public final class SecureSocket { 28 | private static final String[] ENABLED_PROTOCOLS = {"TLSv1.3"}; 29 | private static final String[] APPLICATION_PROTOCOLS = {"mapi/9"}; 30 | 31 | // Cache for the default SSL factory. It must load all trust roots 32 | // so it's worthwhile to cache. 33 | // Only access this through #getDefaultSocketFactory() 34 | private static SSLSocketFactory vanillaFactory = null; 35 | 36 | private static synchronized SSLSocketFactory getDefaultSocketFactory() { 37 | if (vanillaFactory == null) { 38 | vanillaFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); 39 | } 40 | return vanillaFactory; 41 | } 42 | 43 | public static Socket wrap(Target.Validated validated, Socket inner) throws IOException { 44 | Target.Verify verify = validated.connectVerify(); 45 | SSLSocketFactory socketFactory; 46 | boolean checkName = true; 47 | try { 48 | switch (verify) { 49 | case System: 50 | socketFactory = getDefaultSocketFactory(); 51 | break; 52 | case Cert: 53 | KeyStore keyStore = keyStoreForCert(validated.getCert()); 54 | socketFactory = certBasedSocketFactory(keyStore); 55 | break; 56 | case Hash: 57 | socketFactory = hashBasedSocketFactory(validated.connectCertHashDigits()); 58 | checkName = false; 59 | break; 60 | default: 61 | throw new RuntimeException("unreachable: unexpected verification strategy " + verify.name()); 62 | } 63 | return wrapSocket(inner, validated, socketFactory, checkName); 64 | } catch (CertificateException e) { 65 | throw new SSLException("TLS certificate rejected", e); 66 | } 67 | } 68 | 69 | private static SSLSocket wrapSocket(Socket inner, Target.Validated validated, SSLSocketFactory socketFactory, boolean checkName) throws IOException { 70 | SSLSocket sock = (SSLSocket) socketFactory.createSocket(inner, validated.connectTcp(), validated.connectPort(), true); 71 | sock.setUseClientMode(true); 72 | SSLParameters parameters = sock.getSSLParameters(); 73 | 74 | parameters.setProtocols(ENABLED_PROTOCOLS); 75 | 76 | parameters.setServerNames(Collections.singletonList(new SNIHostName(validated.connectTcp()))); 77 | 78 | if (checkName) { 79 | parameters.setEndpointIdentificationAlgorithm("HTTPS"); 80 | } 81 | 82 | // Unfortunately, SSLParameters.setApplicationProtocols is only available 83 | // since language level 9 and currently we're on 8. 84 | // Still call it if it happens to be available. 85 | try { 86 | Method setApplicationProtocols = SSLParameters.class.getMethod("setApplicationProtocols", String[].class); 87 | setApplicationProtocols.invoke(parameters, (Object) APPLICATION_PROTOCOLS); 88 | } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ignored) { 89 | } 90 | 91 | sock.setSSLParameters(parameters); 92 | sock.startHandshake(); 93 | return sock; 94 | } 95 | 96 | private static X509Certificate loadCertificate(String path) throws CertificateException, IOException { 97 | CertificateFactory factory = CertificateFactory.getInstance("X509"); 98 | try (FileInputStream s = new FileInputStream(path)) { 99 | return (X509Certificate) factory.generateCertificate(s); 100 | } 101 | } 102 | 103 | private static SSLSocketFactory certBasedSocketFactory(KeyStore store) throws IOException, CertificateException { 104 | TrustManagerFactory trustManagerFactory; 105 | try { 106 | trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 107 | trustManagerFactory.init(store); 108 | } catch (NoSuchAlgorithmException | KeyStoreException e) { 109 | throw new RuntimeException("Could not create TrustManagerFactory", e); 110 | } 111 | 112 | SSLContext context; 113 | try { 114 | context = SSLContext.getInstance("TLS"); 115 | context.init(null, trustManagerFactory.getTrustManagers(), null); 116 | } catch (NoSuchAlgorithmException | KeyManagementException e) { 117 | throw new RuntimeException("Could not create SSLContext", e); 118 | } 119 | 120 | return context.getSocketFactory(); 121 | } 122 | 123 | private static KeyStore keyStoreForCert(String path) throws IOException, CertificateException { 124 | try { 125 | X509Certificate cert = loadCertificate(path); 126 | KeyStore store = emptyKeyStore(); 127 | store.setCertificateEntry("root", cert); 128 | return store; 129 | } catch (KeyStoreException e) { 130 | throw new RuntimeException("Could not create KeyStore for certificate", e); 131 | } 132 | } 133 | 134 | private static KeyStore emptyKeyStore() throws IOException, CertificateException { 135 | KeyStore store; 136 | try { 137 | store = KeyStore.getInstance("PKCS12"); 138 | store.load(null, null); 139 | return store; 140 | } catch (KeyStoreException | NoSuchAlgorithmException e) { 141 | throw new RuntimeException("Could not create KeyStore for certificate", e); 142 | } 143 | } 144 | 145 | private static SSLSocketFactory hashBasedSocketFactory(String hashDigits) { 146 | TrustManager trustManager = new HashBasedTrustManager(hashDigits); 147 | try { 148 | SSLContext context = SSLContext.getInstance("TLS"); 149 | context.init(null, new TrustManager[]{trustManager}, null); 150 | return context.getSocketFactory(); 151 | } catch (NoSuchAlgorithmException | KeyManagementException e) { 152 | throw new RuntimeException("Could not create SSLContext", e); 153 | } 154 | 155 | } 156 | 157 | private static class HashBasedTrustManager implements X509TrustManager { 158 | private static final char[] HEXDIGITS = "0123456789abcdef".toCharArray(); 159 | private final String hashDigits; 160 | 161 | public HashBasedTrustManager(String hashDigits) { 162 | this.hashDigits = hashDigits; 163 | } 164 | 165 | 166 | @Override 167 | public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { 168 | throw new RuntimeException("this TrustManager is only suitable for client side connections"); 169 | } 170 | 171 | @Override 172 | public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { 173 | X509Certificate cert = x509Certificates[0]; 174 | byte[] certBytes = cert.getEncoded(); 175 | 176 | // for now it's always SHA256. 177 | byte[] hashBytes; 178 | try { 179 | MessageDigest hasher = MessageDigest.getInstance("SHA-256"); 180 | hasher.update(certBytes); 181 | hashBytes = hasher.digest(); 182 | } catch (NoSuchAlgorithmException e) { 183 | throw new RuntimeException("failed to instantiate hash digest"); 184 | } 185 | 186 | // convert to hex digits 187 | StringBuilder buffer = new StringBuilder(2 * hashBytes.length); 188 | for (byte b : hashBytes) { 189 | int hi = (b & 0xF0) >> 4; 190 | int lo = b & 0x0F; 191 | buffer.append(HEXDIGITS[hi]); 192 | buffer.append(HEXDIGITS[lo]); 193 | } 194 | String certDigits = buffer.toString(); 195 | 196 | if (!certDigits.startsWith(hashDigits)) { 197 | throw new CertificateException("Certificate hash does not start with '" + hashDigits + "': " + certDigits); 198 | } 199 | 200 | 201 | } 202 | 203 | @Override 204 | public X509Certificate[] getAcceptedIssuers() { 205 | return new X509Certificate[0]; 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/mcl/net/ValidationError.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.mcl.net; 14 | 15 | @SuppressWarnings("serial") 16 | public final class ValidationError extends Exception { 17 | public ValidationError(String parameter, String message) { 18 | super(parameter + ": " + message); 19 | } 20 | 21 | public ValidationError(String message) { 22 | super(message); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/mcl/parser/HeaderLineParser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.mcl.parser; 14 | 15 | 16 | /** 17 | * The HeaderLineParser is a generic MCLParser that extracts values from 18 | * a metadata header in the MCL protocol either as string or integer values. 19 | * 20 | * @author Fabian Groffen 21 | */ 22 | public final class HeaderLineParser extends MCLParser { 23 | /* types of meta data supported by MCL protocol */ 24 | public final static int NAME = 1; // name of column 25 | public final static int LENGTH = 2; 26 | public final static int TABLE = 3; // may include the schema name 27 | public final static int TYPE = 4; 28 | public final static int TYPESIZES = 5; // precision and scale 29 | 30 | /** The int values found while parsing. Public, you may touch it. */ 31 | public final int intValues[]; 32 | 33 | /** 34 | * Constructs a HeaderLineParser which expects columncount columns. 35 | * 36 | * @param columncount the number of columns in the to be parsed string 37 | */ 38 | public HeaderLineParser(final int columncount) { 39 | super(columncount); 40 | intValues = new int[columncount]; 41 | } 42 | 43 | /** 44 | * Parses the given String source as header line. If source cannot 45 | * be parsed, an MCLParseException is thrown. The columncount argument 46 | * given during construction is used for allocation of the backing array. 47 | * 48 | * @param source a String which should be parsed 49 | * @return the type of the parsed header line 50 | * @throws MCLParseException if an error occurs during parsing 51 | */ 52 | @Override 53 | public int parse(final String source) throws MCLParseException { 54 | final char[] chrLine = source.toCharArray(); 55 | int len = chrLine.length; 56 | int pos = 0; 57 | boolean foundChar = false; 58 | boolean nameFound = false; 59 | int i; 60 | 61 | // find header name searching from the end of the line 62 | for (i = len - 1; i >= 0; i--) { 63 | switch (chrLine[i]) { 64 | case ' ': 65 | case '\n': 66 | case '\t': 67 | case '\r': 68 | if (!foundChar) { 69 | len = i - 1; 70 | } else { 71 | pos = i + 1; 72 | } 73 | break; 74 | case '#': 75 | // found! 76 | nameFound = true; 77 | if (pos == 0) 78 | pos = i + 1; 79 | i = 0; // force the loop to terminate 80 | break; 81 | default: 82 | foundChar = true; 83 | pos = 0; 84 | break; 85 | } 86 | } 87 | if (!nameFound) 88 | throw new MCLParseException("invalid header, no header name found", pos); 89 | 90 | // depending on the name of the header, we continue 91 | int type = 0; 92 | i = pos; 93 | switch (len - pos) { 94 | case 4: 95 | // source.regionMatches(pos + 1, "name", 0, 4) 96 | if (chrLine[i] == 'n' && chrLine[++i] == 'a' && chrLine[++i] == 'm' && chrLine[++i] == 'e') { 97 | getValues(chrLine, 2, pos - 3); 98 | type = NAME; 99 | } else 100 | // source.regionMatches(pos + 1, "type", 0, 4) 101 | if (chrLine[i] == 't' && chrLine[++i] == 'y' && chrLine[++i] == 'p' && chrLine[++i] == 'e') { 102 | getValues(chrLine, 2, pos - 3); 103 | type = TYPE; 104 | } 105 | break; 106 | case 6: 107 | // source.regionMatches(pos + 1, "length", 0, 6) 108 | if (chrLine[ i ] == 'l' && chrLine[++i] == 'e' && chrLine[++i] == 'n' && chrLine[++i] == 'g' 109 | && chrLine[++i] == 't' && chrLine[++i] == 'h') { 110 | getIntValues(chrLine, 2, pos - 3); 111 | type = LENGTH; 112 | } 113 | break; 114 | case 9: 115 | // System.out.println("In HeaderLineParser.parse() case 9: source line = " + source); 116 | // source.regionMatches(pos + 1, "typesizes", 0, 9) 117 | if (chrLine[ i ] == 't' && chrLine[++i] == 'y' && chrLine[++i] == 'p' && chrLine[++i] == 'e' 118 | && chrLine[++i] == 's' && chrLine[++i] == 'i' && chrLine[++i] == 'z' && chrLine[++i] == 'e' && chrLine[++i] == 's') { 119 | getValues(chrLine, 2, pos - 3); /* these contain precision and scale values (separated by a space), so read them as strings */ 120 | type = TYPESIZES; 121 | } 122 | break; 123 | case 10: 124 | // source.regionMatches(pos + 1, "table_name", 0, 10) 125 | if (chrLine[ i ] == 't' && chrLine[++i] == 'a' && chrLine[++i] == 'b' && chrLine[++i] == 'l' && chrLine[++i] == 'e' 126 | && chrLine[++i] == '_' && chrLine[++i] == 'n' && chrLine[++i] == 'a' && chrLine[++i] == 'm' && chrLine[++i] == 'e') { 127 | getValues(chrLine, 2, pos - 3); 128 | type = TABLE; 129 | } 130 | break; 131 | default: 132 | throw new MCLParseException("unknown header: " + (new String(chrLine, pos, len - pos))); 133 | } 134 | 135 | // adjust colno 136 | colnr = 0; 137 | 138 | return type; 139 | } 140 | 141 | /** 142 | * Fills an array of Strings containing the values between 143 | * ',\t' separators. 144 | * 145 | * As of Oct2014-SP1 release MAPI adds double quotes around names when 146 | * the name contains a comma or a tab or a space or a # or " or \ escape character. 147 | * See issue: https://github.com/MonetDB/MonetDB/issues/3616 148 | * If the parsed name string part has a " as first and last character, 149 | * we remove those added double quotes here. 150 | * 151 | * @param chrLine a character array holding the input data 152 | * @param start where the relevant data starts 153 | * @param stop where the relevant data stops 154 | */ 155 | private final void getValues(final char[] chrLine, int start, final int stop) { 156 | int elem = 0; 157 | boolean inString = false, escaped = false; 158 | 159 | for (int i = start; i < stop; i++) { 160 | switch(chrLine[i]) { 161 | case '\\': 162 | escaped = !escaped; 163 | break; 164 | case '"': 165 | /** 166 | * If all strings are wrapped between two quotes, a \" can 167 | * never exist outside a string. Thus if we believe that we 168 | * are not within a string, we can safely assume we're about 169 | * to enter a string if we find a quote. 170 | * If we are in a string we should stop being in a string if 171 | * we find a quote which is not prefixed by a \, for that 172 | * would be an escaped quote. However, a nasty situation can 173 | * occur where the string is like "test \\" as obvious, a 174 | * test for a \ in front of a " doesn't hold here for all 175 | * cases. Because "test \\\"" can exist as well, we need to 176 | * know if a quote is prefixed by an escaping slash or not. 177 | */ 178 | if (!inString) { 179 | inString = true; 180 | } else if (!escaped) { 181 | inString = false; 182 | } 183 | // reset escaped flag 184 | escaped = false; 185 | break; 186 | case ',': 187 | if (!inString && chrLine[i + 1] == '\t') { 188 | // we found the field separator 189 | if (chrLine[start] == '"') 190 | start++; // skip leading double quote 191 | if (elem < values.length) { 192 | // TODO: also deal with escape characters as done in TupleLineParser.parse() 193 | values[elem++] = new String(chrLine, start, i - (chrLine[i - 1] == '"' ? 1 : 0) - start); 194 | } 195 | i++; 196 | start = i + 1; // reset start for the next name, skipping the field separator (a comma and tab) 197 | } 198 | // reset escaped flag 199 | escaped = false; 200 | break; 201 | default: 202 | escaped = false; 203 | break; 204 | } 205 | } 206 | // add the left over part (last column) 207 | if (chrLine[start] == '"') 208 | start++; // skip leading double quote 209 | if (elem < values.length) 210 | values[elem] = new String(chrLine, start, stop - (chrLine[stop - 1] == '"' ? 1 : 0) - start); 211 | } 212 | 213 | /** 214 | * Fills an array of ints containing the values between 215 | * ',\t' separators. 216 | * 217 | * Feb2017 note - This integer parser doesn't have to parse negative 218 | * numbers, because it is only used to parse column lengths 219 | * which are always greater than 0. 220 | * 221 | * @param chrLine a character array holding the input data 222 | * @param start where the relevant data starts 223 | * @param stop where the relevant data stops 224 | * @throws MCLParseException if an error occurs during parsing 225 | */ 226 | private final void getIntValues(final char[] chrLine, final int start, final int stop) throws MCLParseException { 227 | int elem = 0; 228 | int tmp = 0; 229 | 230 | for (int i = start; i < stop; i++) { 231 | if (chrLine[i] == ',' && chrLine[i + 1] == '\t') { 232 | if (elem < intValues.length) { 233 | intValues[elem++] = tmp; 234 | } 235 | tmp = 0; 236 | i++; 237 | } else { 238 | // note: don't use Character.isDigit() here, because 239 | // we only want ISO-LATIN-1 digits 240 | if (chrLine[i] >= '0' && chrLine[i] <= '9') { 241 | tmp *= 10; 242 | tmp += (int)chrLine[i] - (int)'0'; 243 | } else { 244 | throw new MCLParseException("expected a digit in " + new String(chrLine) + " at " + i); 245 | } 246 | } 247 | } 248 | // add the left over part (last column) 249 | if (elem < intValues.length) 250 | intValues[elem] = tmp; 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/mcl/parser/MCLParseException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.mcl.parser; 14 | 15 | /** 16 | * When an MCLParseException is thrown, the MCL protocol is violated by 17 | * the sender. In general a stream reader throws an 18 | * MCLParseException as soon as something that is read cannot be 19 | * understood or does not conform to the specifications (e.g. a 20 | * missing field). The instance that throws the exception will try to 21 | * give an error offset whenever possible. Alternatively it makes sure 22 | * that the error message includes the offending data read. 23 | */ 24 | @SuppressWarnings("serial") 25 | public final class MCLParseException extends java.text.ParseException { 26 | public MCLParseException(String e) { 27 | super(e, -1); 28 | } 29 | 30 | public MCLParseException(String e, int offset) { 31 | super(e, offset); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/mcl/parser/MCLParser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.mcl.parser; 14 | 15 | 16 | /** 17 | * Interface for parsers in MCL. The parser family in MCL is set up as 18 | * a reusable object. This allows the same parser to be used again for 19 | * the same type of work. While this is a very unnatural solution in 20 | * the Java language, it prevents many object creations on a low level 21 | * of the protocol. This favours performance. 22 | * 23 | * A typical parser has a method parse() which takes a String, and the 24 | * methods hasNext() and next() to retrieve the values that were 25 | * extracted by the parser. Parser specific methods may be available to 26 | * perform common tasks. 27 | * 28 | * @author Fabian Groffen 29 | */ 30 | public abstract class MCLParser { 31 | /** The String values found while parsing. Public, you may touch it. */ 32 | public final String values[]; 33 | protected int colnr = 0; 34 | 35 | /** 36 | * Creates an MCLParser targeted at a given number of field values. 37 | * The lines parsed by an instance of this MCLParser should have 38 | * exactly capacity field values. 39 | * 40 | * @param capacity the number of field values to expect 41 | */ 42 | protected MCLParser(final int capacity) { 43 | values = new String[capacity]; 44 | } 45 | 46 | /** 47 | * Parse the given string, and populate the internal field array 48 | * to allow for next() and hasNext() calls. 49 | * 50 | * @param source the String containing the line to parse 51 | * @return value 52 | * @throws MCLParseException if source cannot be (fully) parsed by 53 | * this parser 54 | * @see #next() 55 | * @see #hasNext() 56 | */ 57 | abstract public int parse(final String source) throws MCLParseException; 58 | 59 | /** 60 | * Repositions the internal field offset to the start, such that the 61 | * next call to next() will return the first field again. 62 | */ 63 | final public void reset() { 64 | colnr = 0; 65 | } 66 | 67 | /** 68 | * Returns whether the next call to next() or nextInt() succeeds. 69 | * 70 | * @return true if the next call to next() or nextInt() is bound to 71 | * succeed 72 | * @see #next() 73 | */ 74 | final public boolean hasNext() { 75 | return colnr < values.length; 76 | } 77 | 78 | /** 79 | * Returns the current field value, and advances the field counter 80 | * to the next value. This method may fail with a RuntimeError if 81 | * the current field counter is out of bounds. Call hasNext() to 82 | * determine if the call to next() will succeed. 83 | * 84 | * @return the current field value 85 | * @see #hasNext() 86 | */ 87 | final public String next() { 88 | return values[colnr++]; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/mcl/parser/StartOfHeaderParser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.mcl.parser; 14 | 15 | import java.nio.Buffer; // needed as some CharBuffer overridden methods (mark() and reset()) return type changed between Java 8 (return Buffer) and 9 (or higher) (return CharBuffer) 16 | import java.nio.CharBuffer; 17 | 18 | /** 19 | * The StartOfHeaderParser allows easy examination of a start of header 20 | * line. It does not fit into the general MCLParser framework because 21 | * it uses a different interface. While the parser is very shallow, it 22 | * requires the caller to know about the header lines that are parsed. 23 | * All this parser does is detect the (valid) type of a soheader, and 24 | * allow to return the fields in it as integer or string. An extra 25 | * bonus is that it can return if another field should be present in the 26 | * soheader. 27 | * 28 | * @author Fabian Groffen 29 | */ 30 | public final class StartOfHeaderParser { 31 | private CharBuffer soh = null; 32 | private int len; 33 | private int pos; 34 | 35 | /* Query types (copied from sql/include/sql_querytype.h) */ 36 | /** A parse response (not handled) */ 37 | public final static int Q_PARSE = '0'; 38 | /** A tabular response (typical ResultSet) */ 39 | public final static int Q_TABLE = '1'; 40 | /** A response to an update statement, contains number of affected 41 | * rows and generated key-id */ 42 | public final static int Q_UPDATE = '2'; 43 | /** A response to a schema update */ 44 | public final static int Q_SCHEMA = '3'; 45 | /** A response to a transaction statement (start, rollback, abort, 46 | * commit) */ 47 | public final static int Q_TRANS = '4'; 48 | /** A tabular response in response to a PREPARE statement containing 49 | * information about the wildcard values that need to be supplied */ 50 | public final static int Q_PREPARE = '5'; 51 | /** A tabular continuation response (for a ResultSet) */ 52 | public final static int Q_BLOCK = '6'; 53 | 54 | 55 | public final int parse(final String in) throws MCLParseException { 56 | soh = CharBuffer.wrap(in); 57 | soh.get(); // skip the & 58 | final int type = soh.get(); 59 | switch (type) { 60 | case Q_PARSE: 61 | case Q_SCHEMA: 62 | len = 0; 63 | break; 64 | case Q_TABLE: 65 | case Q_PREPARE: 66 | len = 4; 67 | soh.get(); 68 | break; 69 | case Q_UPDATE: 70 | len = 2; 71 | soh.get(); 72 | break; 73 | case Q_TRANS: 74 | len = 1; 75 | soh.get(); 76 | break; 77 | case Q_BLOCK: 78 | len = 3; 79 | soh.get(); 80 | break; 81 | default: 82 | throw new MCLParseException("invalid or unknown header", 1); 83 | } 84 | pos = 0; 85 | return type; 86 | } 87 | 88 | /* MvD: disabled hasNext() method as it is never called. 89 | public final boolean hasNext() { 90 | return pos < len; 91 | } 92 | */ 93 | 94 | /** 95 | * Returns the next token in the CharBuffer as integer. The value is 96 | * considered to end at the end of the CharBuffer or at a space. If 97 | * a non-numeric character is encountered an MCLParseException is thrown. 98 | * 99 | * @return The next token in the CharBuffer as integer 100 | * @throws MCLParseException if no numeric value could be read 101 | */ 102 | public final int getNextAsInt() throws MCLParseException { 103 | return (int) getNextAsLong(); 104 | } 105 | 106 | /** 107 | * Returns the next token in the CharBuffer as long integer. The value 108 | * is considered to end at the end of the CharBuffer or at a space. 109 | * If a non-numeric character is encountered an MCLParseException is thrown. 110 | * 111 | * @return The next token in the CharBuffer as long integer 112 | * @throws MCLParseException if no numeric value could be read 113 | */ 114 | public final long getNextAsLong() throws MCLParseException { 115 | pos++; 116 | if (!soh.hasRemaining()) 117 | throw new MCLParseException("unexpected end of string", soh.position() - 1); 118 | 119 | boolean positive = true; 120 | char chr = soh.get(); 121 | // note: don't use Character.isDigit() here, because 122 | // we only want ISO-LATIN-1 digits 123 | if (chr == '-') { 124 | positive = false; 125 | if (!soh.hasRemaining()) 126 | throw new MCLParseException("unexpected end of string", soh.position() - 1); 127 | chr = soh.get(); 128 | } 129 | 130 | long tmp = 0; 131 | if (chr >= '0' && chr <= '9') { 132 | tmp = (int)chr - (int)'0'; 133 | } else { 134 | throw new MCLParseException("expected a digit", soh.position() - 1); 135 | } 136 | 137 | while (soh.hasRemaining() && (chr = soh.get()) != ' ') { 138 | if (chr >= '0' && chr <= '9') { 139 | tmp *= 10; 140 | tmp += (int)chr - (int)'0'; 141 | } else { 142 | throw new MCLParseException("expected a digit", soh.position() - 1); 143 | } 144 | } 145 | 146 | return positive ? tmp : -tmp; 147 | } 148 | 149 | public final String getNextAsString() throws MCLParseException { 150 | pos++; 151 | if (!soh.hasRemaining()) 152 | throw new MCLParseException("unexpected end of string", soh.position() - 1); 153 | 154 | int cnt = 0; 155 | ((Buffer)soh).mark(); 156 | while (soh.hasRemaining() && soh.get() != ' ') { 157 | cnt++; 158 | } 159 | ((Buffer)soh).reset(); 160 | 161 | return soh.subSequence(0, cnt).toString(); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/mcl/parser/TupleLineParser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.mcl.parser; 14 | 15 | /** 16 | * The TupleLineParser extracts the values from a given tuple. 17 | * The number of values that are expected are known upfront to speed up 18 | * allocation and validation. 19 | * 20 | * @author Fabian Groffen 21 | * @author Martin van Dinther 22 | */ 23 | public final class TupleLineParser extends MCLParser { 24 | private StringBuilder uesc = null; // used for building field string value when an escape is present in the field value 25 | 26 | /** 27 | * Constructs a TupleLineParser which expects columncount columns. 28 | * The columncount argument is used for allocation of the public values array. 29 | * While this seems illogical, the caller should know this size, since the 30 | * StartOfHeader contains this information. 31 | * 32 | * @param columncount the number of columns in the to be parsed string 33 | */ 34 | public TupleLineParser(final int columncount) { 35 | super(columncount); 36 | } 37 | 38 | /** 39 | * Parses the given String source as tuple line. 40 | * If source cannot be parsed, a MCLParseException is thrown. 41 | * 42 | * @param source a String representing a tuple line which should be parsed 43 | * @return 0, as there is no 'type' of TupleLine 44 | * @throws MCLParseException if source is not compliant to expected tuple/single value format 45 | */ 46 | @Override 47 | public int parse(final String source) throws MCLParseException { 48 | final int len = source.length(); 49 | if (len <= 0) 50 | throw new MCLParseException("Missing tuple data"); 51 | 52 | // first detect whether this is a single value line (=) or a real tuple ([) 53 | char chr = source.charAt(0); 54 | if (chr == '=') { 55 | if (values.length != 1) 56 | throw new MCLParseException(values.length + 57 | " columns expected, but only single value found"); 58 | 59 | // return the whole string but without the leading = 60 | values[0] = source.substring(1); 61 | 62 | // reset colnr 63 | colnr = 0; 64 | return 0; 65 | } 66 | 67 | if (chr != '[') 68 | throw new MCLParseException("Expected a data row starting with ["); 69 | 70 | // It is a tuple. Extract separate fields by examining the string data char for char 71 | // For parsing it is faster to use an char[] to avoid overhead of source.charAt(i) method calls 72 | final char[] chrLine = source.toCharArray(); 73 | boolean inString = false, escaped = false, fieldHasEscape = false; 74 | int column = 0, cursor = 2; 75 | // scan the characters, when a field separator is found extract the field value as String dealing with possible escape characters 76 | for (int i = 2; i < len; i++) { 77 | switch(chrLine[i]) { 78 | case '\\': 79 | escaped = !escaped; 80 | fieldHasEscape = true; 81 | break; 82 | case '"': 83 | /** 84 | * If all strings are wrapped between two quotes, a \" can 85 | * never exist outside a string. Thus if we believe that we 86 | * are not within a string, we can safely assume we're about 87 | * to enter a string if we find a quote. 88 | * If we are in a string we should stop being in a string if 89 | * we find a quote which is not prefixed by a \, for that 90 | * would be an escaped quote. However, a nasty situation can 91 | * occur where the string is like "test \\" as obvious, a 92 | * test for a \ in front of a " doesn't hold here for all 93 | * cases. Because "test \\\"" can exist as well, we need to 94 | * know if a quote is prefixed by an escaping slash or not. 95 | */ 96 | if (!inString) { 97 | inString = true; 98 | } else if (!escaped) { 99 | inString = false; 100 | } 101 | // reset escaped flag 102 | escaped = false; 103 | break; 104 | case '\t': // potential field separator found 105 | if (!inString && 106 | ((chrLine[i - 1] == ',') || // found field separator: ,\t 107 | ((i + 1 == len - 1) && chrLine[++i] == ']'))) // found last field: \t] 108 | { 109 | // extract the field value as a string, without the potential escape codes 110 | final int endpos = i - 2; // minus the tab and the comma or ] 111 | if (chrLine[cursor] == '"' && 112 | chrLine[endpos] == '"') // field is surrounded by double quotes, so a string with possible escape codes 113 | { 114 | cursor++; 115 | final int fieldlen = endpos - cursor; 116 | if (fieldHasEscape) { 117 | if (uesc == null) { 118 | // first time use, create it with enough capacity, minimum 1024 119 | uesc = new StringBuilder(Math.max(fieldlen, 1024)); 120 | } else { 121 | // reuse the StringBuilder by cleaning it 122 | uesc.setLength(0); 123 | if (fieldlen > 1024) { 124 | // prevent multiple capacity increments during the append()'s in the inner loop 125 | uesc.ensureCapacity(fieldlen); 126 | } 127 | } 128 | // parse the field value (excluding the double quotes) and convert it to a string without any escape characters 129 | for (int pos = cursor; pos < endpos; pos++) { 130 | chr = chrLine[pos]; 131 | if (chr == '\\' && pos + 1 < endpos) { 132 | // we detected an escape 133 | // escapedStr and GDKstrFromStr in gdk_atoms.c only 134 | // support \\ \f \n \r \t \" and \377 135 | pos++; 136 | chr = chrLine[pos]; 137 | switch (chr) { 138 | case 'f': 139 | uesc.append('\f'); 140 | break; 141 | case 'n': 142 | uesc.append('\n'); 143 | break; 144 | case 'r': 145 | uesc.append('\r'); 146 | break; 147 | case 't': 148 | uesc.append('\t'); 149 | break; 150 | case '0': case '1': case '2': case '3': 151 | // this could be an octal number, let's check it out 152 | if (pos + 2 < endpos) { 153 | char chr2 = chrLine[pos + 1]; 154 | char chr3 = chrLine[pos + 2]; 155 | if (chr2 >= '0' && chr2 <= '7' && chr3 >= '0' && chr3 <= '7') { 156 | // we got an octal number between \000 and \377 157 | try { 158 | uesc.append((char)(Integer.parseInt(new String(chrLine, pos, 3), 8))); 159 | pos += 2; 160 | } catch (NumberFormatException e) { 161 | // hmmm, this point should never be reached actually... 162 | throw new AssertionError("Flow error, should never try to parse non-number"); 163 | } 164 | } else { 165 | // do default action if number seems not to be an octal number 166 | uesc.append(chr); 167 | } 168 | } else { 169 | // do default action if number seems not to be an octal number 170 | uesc.append(chr); 171 | } 172 | break; 173 | /* case '\\': optimisation: this code does the same as the default case, so not needed 174 | uesc.append('\\'); 175 | break; 176 | */ 177 | /* case '"': optimisation: this code does the same as the default case, so not needed 178 | uesc.append('"'); 179 | break; 180 | */ 181 | default: 182 | // this is wrong usage of escape (except for '\\' and '"'), just ignore the \-escape and print the char 183 | uesc.append(chr); 184 | break; 185 | } 186 | } else { 187 | uesc.append(chr); 188 | } 189 | } 190 | // put the unescaped string in the right place 191 | values[column] = uesc.toString(); 192 | } else { 193 | // the field is a string surrounded by double quotes and without escape chars 194 | values[column] = new String(chrLine, cursor, fieldlen); 195 | // if (values[column].contains("\\")) { 196 | // throw new MCLParseException("Invalid parsing: detected a \\ in double quoted string: " + fieldVal); 197 | // } 198 | } 199 | } else { 200 | final int vlen = i - 1 - cursor; 201 | if (vlen == 4 && 202 | chrLine[cursor] == 'N' && chrLine[cursor+1] == 'U' && chrLine[cursor+2] == 'L' && chrLine[cursor+3] == 'L') { 203 | // the field contains NULL, so no value 204 | values[column] = null; 205 | } else { 206 | // the field is a string NOT surrounded by double quotes and thus without escape chars 207 | values[column] = new String(chrLine, cursor, vlen); 208 | // if (values[column].contains("\\")) { 209 | // throw new MCLParseException("Invalid parsing: detected a \\ in unquoted string: " + fieldVal); 210 | // } 211 | } 212 | } 213 | cursor = i + 1; 214 | fieldHasEscape = false; // reset for next field scan 215 | column++; 216 | } 217 | // reset escaped flag 218 | escaped = false; 219 | break; 220 | default: 221 | escaped = false; 222 | break; 223 | } // end of switch() 224 | } // end of for() 225 | 226 | // check if this result is of the size we expected it to be 227 | if (column != values.length) 228 | throw new MCLParseException("illegal result length: " + column + "\nlast read: " + (column > 0 ? values[column - 1] : "")); 229 | 230 | // reset colnr 231 | colnr = 0; 232 | return 0; 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/merovingian/MerovingianException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.merovingian; 14 | 15 | /** 16 | * An Exception raised when monetdbd specific problems occur. 17 | * 18 | * This class is a shallow wrapper around Exception to identify an 19 | * exception as one originating from the monetdbd instance being 20 | * communicated with, instead of a locally generated one. 21 | * 22 | * @author Fabian Groffen 23 | * @version 1.0 24 | */ 25 | public class MerovingianException extends Exception { 26 | private static final long serialVersionUID = 101L; 27 | 28 | public MerovingianException(String reason) { 29 | super(reason); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/merovingian/SabaothDB.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.merovingian; 14 | 15 | import java.util.Date; 16 | 17 | /** 18 | * Implementation of the Sabaoth C-struct as Java object. 19 | * 20 | * This Class implements a parser for the string representation of a 21 | * sabaoth information struct as returned by monetdbd. 22 | * 23 | * Currently this class implements version 1 and 2 of the sabdb 24 | * serialisation. 25 | * 26 | * @version 2.0 27 | */ 28 | public class SabaothDB { 29 | /** The name of the database */ 30 | private String dbname; 31 | /** The URI how to connect to this database, or null if not 32 | * shared */ 33 | private String uri; 34 | /** Whether or not the database is under maintenance */ 35 | private boolean locked; 36 | /** The state of this database, one of SABdbState */ 37 | private SABdbState state; 38 | /** A list of Strings representing the available scenarios of this 39 | * database */ 40 | private String[] scenarios; 41 | /** The number of times this database was started */ 42 | private int startCounter; 43 | /** The number of times this database was stopped */ 44 | private int stopCounter; 45 | /** The number of times this database has crashed */ 46 | private int crashCounter; 47 | /** The number of seconds this database was running on average */ 48 | private long avgUptime; 49 | /** The maximum number of seconds this database was running */ 50 | private long maxUptime; 51 | /** The minimum number of seconds this database was running */ 52 | private long minUptime; 53 | /** The last time this database crashed, null if never */ 54 | private Date lastCrash; 55 | /** The last time this database was started, null if never */ 56 | private Date lastStart; 57 | /** The last time this database was stopped, null if never */ 58 | private Date lastStop; 59 | /** Whether the last start was a crash */ 60 | private boolean crashAvg1; 61 | /** Average of crashes in the last 10 start attempts */ 62 | private double crashAvg10; 63 | /** Average of crashes in the last 30 start attempts */ 64 | private double crashAvg30; 65 | 66 | /** The serialised format header */ 67 | private final String sabdbhdr = "sabdb:"; 68 | 69 | /** Sabaoth state enumeration */ 70 | public enum SABdbState { 71 | SABdbIllegal (0), 72 | SABdbRunning (1), 73 | SABdbCrashed (2), 74 | SABdbInactive(3), 75 | SABdbStarting(4); 76 | 77 | private final int cValue; 78 | 79 | SABdbState(int cValue) { 80 | this.cValue = cValue; 81 | } 82 | 83 | public int getcValue() { 84 | return(cValue); 85 | } 86 | 87 | static SABdbState getInstance(int val) throws IllegalArgumentException { 88 | /* using this syntax because I can't find examples that do 89 | * it without it */ 90 | for (SABdbState s : SABdbState.values()) { 91 | if (s.getcValue() == val) { 92 | return(s); 93 | } 94 | } 95 | throw new IllegalArgumentException("No such state with value: " 96 | + val); 97 | } 98 | } 99 | 100 | /** 101 | * Constructs a new SabaothDB object from a String. 102 | * 103 | * @param sabdb the serialised sabdb C-struct 104 | */ 105 | public SabaothDB(String sabdb) 106 | throws IllegalArgumentException 107 | { 108 | if (sabdb == null) 109 | throw new IllegalArgumentException("String is null"); 110 | if (!sabdb.startsWith(sabdbhdr)) 111 | throw new IllegalArgumentException("String is not a Sabaoth struct"); 112 | String[] parts = sabdb.split(":", 3); 113 | if (parts.length != 3) 114 | throw new IllegalArgumentException("String seems incomplete, " + 115 | "expected 3 components, only found " + parts.length); 116 | int protover; 117 | try { 118 | protover = Integer.parseInt(parts[1]); 119 | } catch (NumberFormatException e) { 120 | throw new IllegalArgumentException("Illegal protocol version: " + 121 | parts[1]); 122 | } 123 | if (protover != 1 && protover != 2) 124 | throw new IllegalArgumentException("Unsupported protocol version: " + protover); 125 | sabdb = parts[2]; 126 | parts = sabdb.split(",", -2); 127 | if (protover == 1 && parts.length != 16 || protover == 2 && parts.length != 17) 128 | throw new IllegalArgumentException("String seems wrong, " + 129 | "unexpected number of components: " + parts.length); 130 | 131 | /* Sabaoth sabdb struct */ 132 | int pc = 0; 133 | this.dbname = parts[pc++]; 134 | if (protover == 1) { 135 | this.uri = ""; 136 | int lastslash = this.dbname.lastIndexOf('/'); 137 | if (lastslash == -1) 138 | throw new IllegalArgumentException("invalid path (needs slash): " + this.dbname); 139 | this.dbname = this.dbname.substring(lastslash + 1); 140 | } else { 141 | this.uri = parts[pc++]; 142 | } 143 | this.locked = parts[pc++].equals("1") ? true : false; 144 | this.state = SABdbState.getInstance(Integer.parseInt(parts[pc++])); 145 | this.scenarios = parts[pc++].split("'"); 146 | if (protover == 1) /* skip connections */ 147 | pc++; 148 | /* Sabaoth sabuplog struct */ 149 | this.startCounter = Integer.parseInt(parts[pc++]); 150 | this.stopCounter = Integer.parseInt(parts[pc++]); 151 | this.crashCounter = Integer.parseInt(parts[pc++]); 152 | this.avgUptime = Long.parseLong(parts[pc++]); 153 | this.maxUptime = Long.parseLong(parts[pc++]); 154 | this.minUptime = Long.parseLong(parts[pc++]); 155 | long t = Long.parseLong(parts[pc++]); 156 | if (t == -1) { 157 | this.lastCrash = null; 158 | } else { 159 | this.lastCrash = new Date(t * 1000); 160 | } 161 | t = Long.parseLong(parts[pc++]); 162 | if (t == -1) { 163 | this.lastStart = null; 164 | } else { 165 | this.lastStart = new Date(t * 1000); 166 | } 167 | if (protover != 1) { 168 | t = Long.parseLong(parts[pc++]); 169 | if (t == -1) { 170 | this.lastStop = null; 171 | } else { 172 | this.lastStop = new Date(t * 1000); 173 | } 174 | } else { 175 | this.lastStop = null; 176 | } 177 | this.crashAvg1 = parts[pc++].equals("1") ? true : false; 178 | this.crashAvg10 = Double.parseDouble(parts[pc++]); 179 | this.crashAvg30 = Double.parseDouble(parts[pc++]); 180 | } 181 | 182 | public String getName() { 183 | return(dbname); 184 | } 185 | 186 | public String getURI() { 187 | return(uri); 188 | } 189 | 190 | public boolean isLocked() { 191 | return(locked); 192 | } 193 | 194 | public SABdbState getState() { 195 | return(state); 196 | } 197 | 198 | public String[] getScenarios() { 199 | return(scenarios); 200 | } 201 | 202 | public int getStartCount() { 203 | return(startCounter); 204 | } 205 | 206 | public int getStopCount() { 207 | return(stopCounter); 208 | } 209 | 210 | public int getCrashCount() { 211 | return(crashCounter); 212 | } 213 | 214 | public long getAverageUptime() { 215 | return(avgUptime); 216 | } 217 | 218 | public long getMaximumUptime() { 219 | return(maxUptime); 220 | } 221 | 222 | public long getMinimumUptime() { 223 | return(minUptime); 224 | } 225 | 226 | public Date lastCrashed() { 227 | return(lastCrash); 228 | } 229 | 230 | public Date lastStarted() { 231 | return(lastStart); 232 | } 233 | 234 | public Date lastStopped() { 235 | return(lastStop); 236 | } 237 | 238 | public boolean lastStartWasSuccessful() { 239 | return(crashAvg1); 240 | } 241 | 242 | public double getCrashAverage10() { 243 | return(crashAvg10); 244 | } 245 | 246 | public double getCrashAverage30() { 247 | return(crashAvg30); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/util/Exporter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.util; 14 | 15 | import org.monetdb.jdbc.MonetWrapper; // for dq() and sq() 16 | 17 | import java.io.PrintWriter; 18 | import java.sql.ResultSet; 19 | import java.sql.SQLException; 20 | 21 | public abstract class Exporter { 22 | protected PrintWriter out; 23 | protected boolean useSchema; 24 | 25 | protected Exporter(final PrintWriter out) { 26 | this.out = out; 27 | } 28 | 29 | public abstract void dumpSchema( 30 | final java.sql.DatabaseMetaData dbmd, 31 | final String type, 32 | final String schema, 33 | final String name) throws SQLException; 34 | 35 | public abstract void dumpResultSet(final ResultSet rs) throws SQLException; 36 | 37 | public abstract void setProperty(final int type, final int value) throws Exception; 38 | public abstract int getProperty(final int type) throws Exception; 39 | 40 | 41 | //=== shared utilities 42 | 43 | public final void useSchemas(final boolean use) { 44 | useSchema = use; 45 | } 46 | 47 | /** 48 | * Convenience function to call the general utility function MonetWrapper.dq() 49 | * to add double quotes around an SQL Identifier such as column or 50 | * table or schema name in SQL queries. 51 | * It also adds escapes for special characters: double quotes and the escape character 52 | * 53 | * FYI: it is made public as it is also called from client/JdbcClient.java 54 | * 55 | * @param in the string to quote 56 | * @return the quoted string 57 | */ 58 | public static final String dq(final String in) { 59 | return MonetWrapper.dq(in); 60 | } 61 | 62 | /** 63 | * Convenience function to call the general utility function MonetWrapper.sq() 64 | * to add single quotes around string literals in SQL queries. 65 | * It also adds escapes for special characters: single quotes and the escape character 66 | * 67 | * @param in the string to quote 68 | * @return the quoted string 69 | */ 70 | protected static final String sq(final String in) { 71 | return MonetWrapper.sq(in); 72 | } 73 | 74 | /** 75 | * Simple helper function to repeat a given character a number of times. 76 | * 77 | * @param chr the character to repeat 78 | * @param cnt the number of times to repeat chr 79 | * @return a String holding cnt times chr 80 | */ 81 | protected static final String repeat(final char chr, final int cnt) { 82 | final char[] buf = new char[cnt]; 83 | java.util.Arrays.fill(buf, chr); 84 | return new String(buf); 85 | } 86 | 87 | /** 88 | * Utility method to fetch the "query" column value from sys.tables for a specific view or table in a specific schema 89 | * The "query" column value contains the original SQL view creation text or the ON clause text when it is a REMOTE TABLE 90 | * 91 | * @param con the JDBC connection, may not be null 92 | * @param schema the schema name, may not be null or empty 93 | * @param name the view or table name, may not be null or empty 94 | * @return the value of the "query" field for the specified view/table name and schema. It can return null. 95 | */ 96 | protected static final String fetchSysTablesQueryValue( 97 | final java.sql.Connection con, 98 | final String schema, 99 | final String name) 100 | { 101 | java.sql.Statement stmt = null; 102 | ResultSet rs = null; 103 | String val = null; 104 | try { 105 | stmt = con.createStatement(); 106 | final String cmd = "SELECT query FROM sys.tables WHERE name = " + sq(name) 107 | + " and schema_id IN (SELECT id FROM sys.schemas WHERE name = " + sq(schema) + ")"; 108 | rs = stmt.executeQuery(cmd); 109 | if (rs != null) { 110 | if (rs.next()) { 111 | val = rs.getString(1); 112 | } 113 | } 114 | } catch (SQLException se) { 115 | /* ignore */ 116 | } finally { 117 | // free resources 118 | if (rs != null) { 119 | try { rs.close(); } catch (SQLException se) { /* ignore */ } 120 | } 121 | if (stmt != null) { 122 | try { stmt.close(); } catch (SQLException se) { /* ignore */ } 123 | } 124 | } 125 | return val; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/util/Extract.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.util; 14 | 15 | import java.io.BufferedReader; 16 | import java.io.FileNotFoundException; 17 | import java.io.FileWriter; 18 | import java.io.IOException; 19 | 20 | /** 21 | * This file contains a function to extract files from its including Jar 22 | * package. 23 | * 24 | * @author Ying Zhang "Y.Zhang@cwi.nl" 25 | * @version 0.1 26 | */ 27 | public class Extract { 28 | private static final int DEFAULT_BUFSIZE = 16386; 29 | 30 | public Extract() {} 31 | 32 | /** 33 | * Extracts a file from the Jar package which includes this class to 34 | * the given destination 35 | * @param fromFile The file to extract, including it absolute path 36 | * inside its including Jar package. 37 | * @param toFile Destination for the extracted file 38 | * @throws FileNotFoundException If the file to extract can not be 39 | * found in its including Jar package. 40 | * @throws IOException If any error happens during 41 | * creating/reading/writing files. 42 | */ 43 | public static void extractFile(final String fromFile, final String toFile) 44 | throws FileNotFoundException, IOException 45 | { 46 | final java.io.InputStream is = new Extract().getClass().getResourceAsStream(fromFile); 47 | if (is == null) { 48 | throw new FileNotFoundException("File " + fromFile + 49 | " does not exist in the JAR package."); 50 | } 51 | 52 | final BufferedReader reader = new BufferedReader(new java.io.InputStreamReader(is)); 53 | final FileWriter writer = new FileWriter(toFile, false); 54 | final char[] cbuf = new char[DEFAULT_BUFSIZE]; 55 | int ret = reader.read(cbuf, 0, DEFAULT_BUFSIZE); 56 | while (ret > 0) { 57 | writer.write(cbuf, 0, ret); 58 | ret = reader.read(cbuf, 0, DEFAULT_BUFSIZE); 59 | } 60 | reader.close(); 61 | writer.close(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/util/FileTransferHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.util; 14 | 15 | import org.monetdb.jdbc.MonetConnection; 16 | 17 | import java.io.BufferedReader; 18 | import java.io.BufferedWriter; 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | import java.io.InputStreamReader; 22 | import java.io.OutputStream; 23 | import java.io.OutputStreamWriter; 24 | import java.nio.charset.Charset; 25 | import java.nio.charset.StandardCharsets; 26 | import java.nio.file.FileSystems; 27 | import java.nio.file.Files; 28 | import java.nio.file.Path; 29 | import java.nio.file.StandardOpenOption; 30 | import java.util.zip.GZIPInputStream; 31 | import java.util.zip.GZIPOutputStream; 32 | 33 | /** 34 | * Default implementation of UploadHandler and DownloadHandler interfaces 35 | * for reading from and writing to files on the local file system. 36 | * It enables support for: 37 | * COPY .. INTO table FROM 'file-name' ON CLIENT ... 38 | * and 39 | * COPY SELECT_query INTO 'file-name' ON CLIENT ... 40 | * handling. 41 | * 42 | * Currently only file compression format .gz is supported. This is intentionally 43 | * as other compression formats would introduce dependencies on external 44 | * libraries which complicates usage of JDBC driver or JdbcClient application. 45 | * Developers can of course build their own MyFileTransferHandler class 46 | * and use it instead of this default implementation. 47 | * 48 | * A FileTransferHandler object needs to be registered via 49 | * {@link MonetConnection#setUploadHandler(MonetConnection.UploadHandler)} and/or 50 | * {@link MonetConnection#setDownloadHandler(MonetConnection.DownloadHandler)}. 51 | * 52 | * @author Joeri van Ruth 53 | * @author Martin van Dinther 54 | * @version 1.1 55 | */ 56 | public class FileTransferHandler implements MonetConnection.UploadHandler, MonetConnection.DownloadHandler { 57 | private final Path root; 58 | private final Charset encoding; 59 | 60 | /** 61 | * Create a new FileTransferHandler which serves the given directory. 62 | * 63 | * @param dir directory Path to read and write files from 64 | * @param encoding the specified characterSet encoding is used for all data files in the directory 65 | * when null the Charset.defaultCharset() is used. 66 | */ 67 | public FileTransferHandler(final Path dir, final Charset encoding) { 68 | this.root = dir.toAbsolutePath().normalize(); 69 | this.encoding = encoding != null ? encoding: Charset.defaultCharset(); 70 | } 71 | 72 | /** 73 | * Create a new FileTransferHandler which serves the given directory. 74 | * 75 | * @param dir directory String to read and write files from 76 | * @param encoding the specified characterSet encoding is used for all data files in the directory 77 | * when null the Charset.defaultCharset() is used. 78 | */ 79 | public FileTransferHandler(final String dir, final Charset encoding) { 80 | this(FileSystems.getDefault().getPath(dir), encoding); 81 | } 82 | 83 | /** 84 | * Read the data from the specified file (in the root directory) and upload it to the server. 85 | */ 86 | public void handleUpload(final MonetConnection.Upload handle, final String name, final boolean textMode, final long linesToSkip) throws IOException { 87 | if (name == null || name.isEmpty()) { 88 | handle.sendError("Missing file name"); 89 | return; 90 | } 91 | final Path path = root.resolve(name).normalize(); 92 | if (!path.startsWith(root)) { 93 | handle.sendError("File is not in upload directory: " + root.toString()); 94 | return; 95 | } 96 | if (!Files.isReadable(path)) { 97 | handle.sendError("Cannot read file " + path.toString()); 98 | return; 99 | } 100 | 101 | // In this implementation we ONLY support gzip compression format and none of the other compression formats. 102 | if (name.endsWith(".bz2") || name.endsWith(".lz4") || name.endsWith(".xz") || name.endsWith(".zip")) { 103 | final String extension = name.substring(name.lastIndexOf('.')); 104 | handle.sendError("Specified file compression format " + extension + " is not supported. Only .gz is supported."); 105 | return; 106 | } 107 | 108 | InputStream byteStream = Files.newInputStream(path); 109 | if (name.endsWith(".gz")) { 110 | byteStream = new GZIPInputStream(byteStream, 128 * 1024); 111 | } 112 | 113 | if (!textMode || (linesToSkip == 0 && utf8Encoded())) { 114 | // when !textMode we must upload as a byte stream 115 | // when utf8Encoded and linesToSkip is 0 it is more efficient to upload as a byte stream 116 | handle.uploadFrom(byteStream); 117 | byteStream.close(); 118 | } else { 119 | // cannot upload as a byte stream, must deal with encoding and/or linesToSkip 120 | final BufferedReader reader = new BufferedReader(new InputStreamReader(byteStream, encoding)); 121 | handle.uploadFrom(reader, linesToSkip); 122 | reader.close(); 123 | } 124 | } 125 | 126 | /** 127 | * Download the data from the server and write it to a new created file in the root directory. 128 | * When a file with the same name already exists the download request will send an error and NOT overwrite the existing file. 129 | */ 130 | public void handleDownload(final MonetConnection.Download handle, final String name, final boolean textMode) throws IOException { 131 | if (name == null || name.isEmpty()) { 132 | handle.sendError("Missing file name"); 133 | return; 134 | } 135 | final Path path = root.resolve(name).normalize(); 136 | if (!path.startsWith(root)) { 137 | handle.sendError("File is not in download directory: " + root.toString()); 138 | return; 139 | } 140 | if (Files.exists(path)) { 141 | handle.sendError("File already exists: " + path.toString()); 142 | return; 143 | } 144 | 145 | // In this implementation we ONLY support gzip compression format and none of the other compression formats. 146 | if (name.endsWith(".bz2") || name.endsWith(".lz4") || name.endsWith(".xz") || name.endsWith(".zip")) { 147 | final String extension = name.substring(name.lastIndexOf('.')); 148 | handle.sendError("Requested file compression format " + extension + " is not supported. Use .gz instead."); 149 | return; 150 | } 151 | 152 | OutputStream byteStream = Files.newOutputStream(path, StandardOpenOption.CREATE_NEW); 153 | if (name.endsWith(".gz")) { 154 | byteStream = new GZIPOutputStream(byteStream, 128 * 1024); 155 | } 156 | 157 | if (!textMode || utf8Encoded()) { 158 | // when !textMode we must download as a byte stream 159 | // when utf8Encoded it is more efficient to download as a byte stream 160 | handle.downloadTo(byteStream); 161 | byteStream.close(); 162 | } else { 163 | // cannot download as a byte stream, must deal with encoding 164 | final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(byteStream, encoding)); 165 | handle.downloadTo(writer); 166 | writer.close(); 167 | } 168 | } 169 | 170 | public boolean utf8Encoded() { 171 | return encoding.equals(StandardCharsets.UTF_8); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/util/OptionsException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.util; 14 | 15 | public final class OptionsException extends Exception { 16 | static final long serialVersionUID = 42L; // needed to prevent: warning: [serial] serializable class OptionsException has no definition of serialVersionUID 17 | 18 | public OptionsException(final String reason) { 19 | super(reason); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/monetdb/util/SQLRestore.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | package org.monetdb.util; 14 | 15 | import java.io.BufferedReader; 16 | import java.io.File; 17 | import java.io.IOException; 18 | import java.util.concurrent.atomic.AtomicBoolean; 19 | 20 | import org.monetdb.mcl.io.BufferedMCLReader; 21 | import org.monetdb.mcl.io.BufferedMCLWriter; 22 | import org.monetdb.mcl.io.LineType; 23 | import org.monetdb.mcl.net.MapiSocket; 24 | 25 | /** 26 | * Use this class to restore an SQL dump file. 27 | */ 28 | public final class SQLRestore { 29 | 30 | private final String _host; 31 | private final int _port; 32 | private final String _user; 33 | private final String _password; 34 | private final String _dbName; 35 | 36 | public SQLRestore(final String host, final int port, final String user, final String password, final String dbName) throws IOException { 37 | if (host == null || user == null || password == null || dbName == null) 38 | throw new NullPointerException(); 39 | _host = host; 40 | _port = port; 41 | _user = user; 42 | _password = password; 43 | _dbName = dbName; 44 | } 45 | 46 | private static final class ServerResponseReader implements Runnable { 47 | private final BufferedMCLReader _is; 48 | private final AtomicBoolean _errorState = new AtomicBoolean(false); 49 | private String _errorMessage = null; 50 | 51 | ServerResponseReader(final BufferedMCLReader is) { 52 | _is = is; 53 | } 54 | 55 | public void run() { 56 | try { 57 | while (true) { 58 | _is.advance(); 59 | final String line = _is.getLine(); 60 | if (line == null) 61 | break; 62 | final LineType result = _is.getLineType(); 63 | switch (result) { 64 | case ERROR: 65 | _errorMessage = line; 66 | _errorState.set(true); 67 | return; 68 | default: 69 | // do nothing... 70 | } 71 | } 72 | } catch (IOException e) { 73 | _errorMessage = e.getMessage(); 74 | _errorState.set(true); 75 | } finally { 76 | try { 77 | _is.close(); 78 | } catch (IOException e) { 79 | // ignore errors 80 | } 81 | } 82 | } 83 | 84 | /** 85 | * Check whether the server has responded with an error. 86 | * Any error is regarded as fatal. 87 | * @return whether the server has responded with an error 88 | */ 89 | public boolean inErrorState() { 90 | return _errorState.get(); 91 | } 92 | 93 | /** 94 | * Get error message if inErrorState() is true. 95 | * Behaviour is not defined if called before inErrorState is true. 96 | * @return the error message 97 | */ 98 | public String getErrorMessage() { 99 | return _errorMessage; 100 | } 101 | } 102 | 103 | /** 104 | * Restores a given SQL dump to the database. 105 | * 106 | * @param source file object 107 | * @throws IOException when IO exception occurred 108 | */ 109 | public void restore(final File source) throws IOException { 110 | final MapiSocket ms = new MapiSocket(); 111 | try { 112 | ms.setLanguage("sql"); 113 | ms.setDatabase(_dbName); 114 | ms.connect(_host, _port, _user, _password); 115 | 116 | final BufferedMCLWriter os = ms.getWriter(); 117 | final BufferedMCLReader reader = ms.getReader(); 118 | 119 | final ServerResponseReader srr = new ServerResponseReader(reader); 120 | 121 | final Thread responseReaderThread = new Thread(srr); 122 | responseReaderThread.start(); 123 | try { 124 | // FIXME: we assume here that the dump is in system's default encoding 125 | final BufferedReader sourceData = new BufferedReader(new java.io.FileReader(source)); 126 | try { 127 | os.write('s'); // signal that a new statement (or series of) is coming 128 | while(!srr.inErrorState()) { 129 | final char[] buf = new char[4096]; 130 | final int result = sourceData.read(buf); 131 | if (result < 0) 132 | break; 133 | os.write(buf, 0, result); 134 | } 135 | 136 | os.flush(); // mark the end of the statement (or series of) 137 | os.close(); 138 | } finally { 139 | sourceData.close(); 140 | } 141 | } finally { 142 | try { 143 | responseReaderThread.join(); 144 | } catch (InterruptedException e) { 145 | throw new IOException(e.getMessage()); 146 | } 147 | 148 | // if the server signalled an error, we should respect it... 149 | if (srr.inErrorState()) { 150 | throw new IOException(srr.getErrorMessage()); 151 | } 152 | } 153 | } catch (org.monetdb.mcl.MCLException e) { 154 | throw new IOException(e.getMessage()); 155 | } catch (org.monetdb.mcl.parser.MCLParseException e) { 156 | throw new IOException(e.getMessage()); 157 | } finally { 158 | ms.close(); 159 | } 160 | } 161 | 162 | public void close() { 163 | // do nothing at the moment... 164 | } 165 | 166 | 167 | public static void main(String[] args) throws IOException { 168 | if (args.length != 6) { 169 | System.err.println("USAGE: java " + SQLRestore.class.getName() + 170 | " "); 171 | System.exit(1); 172 | } 173 | 174 | // parse arguments 175 | final String host = args[0]; 176 | final int port = Integer.parseInt(args[1]); // FIXME: catch NumberFormatException 177 | final String user = args[2]; 178 | final String password = args[3]; 179 | final String dbName = args[4]; 180 | final File dumpFile = new File(args[5]); 181 | 182 | // check arguments 183 | if (!dumpFile.isFile() || !dumpFile.canRead()) { 184 | System.err.println("Cannot read: " + dumpFile); 185 | System.exit(1); 186 | } 187 | 188 | final SQLRestore md = new SQLRestore(host, port, user, password, dbName); 189 | try { 190 | System.out.println("Start restoring " + dumpFile); 191 | long duration = -System.currentTimeMillis(); 192 | md.restore(dumpFile); 193 | duration += System.currentTimeMillis(); 194 | System.out.println("Restoring took: " + duration + "ms"); 195 | } finally { 196 | md.close(); 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /tests/ConnectionTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | import java.sql.Connection; 14 | import java.sql.DriverManager; 15 | import java.sql.ResultSet; 16 | import java.sql.SQLException; 17 | import java.sql.Statement; 18 | import java.util.Properties; 19 | import java.util.SimpleTimeZone; 20 | import java.util.TimeZone; 21 | 22 | public final class ConnectionTests { 23 | 24 | private final String url; 25 | private final Properties connProps; 26 | private final TimeZone timeZone; 27 | 28 | public ConnectionTests(String url, Properties props, TimeZone timeZone) { 29 | this.url = url; 30 | Properties myProps = null; 31 | if (props != null) { 32 | myProps = new Properties(); 33 | myProps.putAll(props); 34 | } 35 | this.connProps = myProps; 36 | this.timeZone = timeZone; 37 | } 38 | 39 | public ConnectionTests(String url) { 40 | this(url, null, null); 41 | } 42 | 43 | public ConnectionTests withSuffix(String suffix) { 44 | String newUrl = url; 45 | 46 | if (newUrl.contains("?")) { 47 | newUrl += "&"; 48 | } else { 49 | newUrl += "?"; 50 | } 51 | newUrl += suffix; 52 | 53 | return new ConnectionTests(newUrl, this.connProps, this.timeZone); 54 | } 55 | 56 | public ConnectionTests withProp(String key, String value) { 57 | ConnectionTests sub = new ConnectionTests(this.url, new Properties(), this.timeZone); 58 | if (this.connProps != null) 59 | sub.connProps.putAll(this.connProps); 60 | sub.connProps.setProperty(key, value); 61 | return sub; 62 | } 63 | 64 | public ConnectionTests withTimeZone(int offsetMinutes) { 65 | TimeZone tz = new SimpleTimeZone(offsetMinutes * 60 * 1000, "Custom" + offsetMinutes); 66 | return new ConnectionTests(this.url, this.connProps, tz); 67 | } 68 | 69 | public static void main(String[] args) throws SQLException, Failure { 70 | String url = args[0]; 71 | runTests(url); 72 | } 73 | 74 | public static void runTests(String url) throws SQLException, Failure { 75 | ConnectionTests tester = new ConnectionTests(url); 76 | 77 | tester.checkAutoCommit(true); 78 | tester.withSuffix("autocommit=true").checkAutoCommit(true); 79 | tester.withSuffix("autocommit=false").checkAutoCommit(false); 80 | tester.withProp("autocommit", "true").checkAutoCommit(true); 81 | tester.withProp("autocommit", "false").checkAutoCommit(false); 82 | 83 | tester.testTimeZone(); 84 | 85 | tester.cleanup(); 86 | } 87 | 88 | Connection connect() throws SQLException { 89 | TimeZone restore = null; 90 | try { 91 | if (this.timeZone != null) { 92 | restore = TimeZone.getDefault(); 93 | TimeZone.setDefault(this.timeZone); 94 | } 95 | if (connProps != null) { 96 | return DriverManager.getConnection(url, connProps); 97 | } else { 98 | return DriverManager.getConnection(url); 99 | } 100 | } finally { 101 | if (restore != null) { 102 | TimeZone.setDefault(restore); 103 | } 104 | } 105 | } 106 | 107 | private void checkAutoCommit(boolean expectAutocommit) throws SQLException, Failure { 108 | // Create and fill the table, leave one row uncommitted. 109 | try (Connection conn = connect(); Statement stmt = conn.createStatement()) { 110 | // Does the connection itself believe to be in the correct mode? 111 | boolean autocommitEnabled = conn.getAutoCommit(); 112 | if (autocommitEnabled != expectAutocommit) { 113 | throw new Failure("Expected autocommit to start as " + expectAutocommit + ", got " + autocommitEnabled); 114 | } 115 | 116 | // Let's test how it works out in practice 117 | stmt.execute("DROP TABLE IF EXISTS connectiontests"); 118 | stmt.execute("CREATE TABLE connectiontests(i INT)"); 119 | stmt.execute("INSERT INTO connectiontests VALUES (42)"); 120 | if (!expectAutocommit) 121 | conn.commit(); 122 | // This will only be committed in autocommit mode 123 | stmt.execute("INSERT INTO connectiontests VALUES (99)"); 124 | } 125 | 126 | // Check whether the uncommitted row is there or not 127 | try (Connection conn = connect(); Statement stmt = conn.createStatement()) { 128 | try (ResultSet rs = stmt.executeQuery("SELECT COUNT(i) FROM connectiontests")) { 129 | rs.next(); 130 | int n = rs.getInt(1); 131 | if (expectAutocommit) { 132 | if (n != 2) { 133 | throw new Failure("Expected 2 rows because autocommit should be on, got " + n); 134 | } 135 | } else { 136 | if (n != 1) { 137 | throw new Failure("Expected 1 row because autocommit should be off, got " + n); 138 | } 139 | } 140 | } 141 | } 142 | } 143 | 144 | private void testTimeZone() throws SQLException, Failure { 145 | try (Connection conn = connect(); Statement stmt = conn.createStatement()) { 146 | stmt.execute("DROP TABLE IF EXISTS connectiontests_ts"); 147 | stmt.execute("CREATE TABLE connectiontests_ts(ts TIMESTAMP WITH TIME ZONE)"); 148 | stmt.execute("INSERT INTO connectiontests_ts VALUES (str_to_timestamp(100, '%s'))"); 149 | } 150 | 151 | this.withTimeZone(0).verifyTimeZoneSuffix("+00:00"); 152 | this.withTimeZone(240).verifyTimeZoneSuffix("+04:00"); 153 | this.withTimeZone(270).verifyTimeZoneSuffix("+04:30"); 154 | } 155 | 156 | private void verifyTimeZoneSuffix(String suffix) throws SQLException, Failure { 157 | try (Connection conn = connect(); Statement stmt = conn.createStatement()) { 158 | ResultSet rs = stmt.executeQuery("SELECT * FROM connectiontests_ts"); 159 | rs.next(); 160 | String s = rs.getString(1); 161 | if (!s.endsWith(suffix)) { 162 | String msg = String.format("Expected suffix '%s', got timestamp '%s'", suffix, s); 163 | throw new Failure(msg); 164 | } 165 | } 166 | } 167 | 168 | private void cleanup() throws SQLException { 169 | try (Connection conn = connect(); Statement stmt = conn.createStatement()) { 170 | stmt.execute("DROP TABLE IF EXISTS connectiontests"); 171 | stmt.execute("DROP TABLE IF EXISTS connectiontests_ts"); 172 | } 173 | } 174 | 175 | public class Failure extends Exception { 176 | private static final long serialVersionUID = 1L; 177 | 178 | public Failure(String msg) { 179 | super(msg); 180 | } 181 | 182 | @Override 183 | public String getMessage() { 184 | StringBuilder msg = new StringBuilder(); 185 | msg.append("When connected to "); 186 | msg.append(url); 187 | if (timeZone != null) { 188 | msg.append(", in time zone "); 189 | msg.append(timeZone.getID()); 190 | msg.append(" ("); 191 | msg.append(timeZone.getDisplayName()); 192 | msg.append(")"); 193 | } else { 194 | msg.append(", in the default time zone"); 195 | } 196 | if (connProps != null) { 197 | msg.append(", with "); 198 | msg.append(connProps.size()); 199 | msg.append(" properties"); 200 | connProps.forEach((k, v) -> { 201 | msg.append(", "); 202 | msg.append(k); 203 | msg.append("="); 204 | msg.append(v); 205 | }); 206 | } 207 | msg.append(": "); 208 | msg.append(super.getMessage()); 209 | return msg.toString(); 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /tests/SQLcopyinto.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | import java.sql.*; 14 | import java.util.*; 15 | import org.monetdb.mcl.net.MapiSocket; 16 | import org.monetdb.mcl.io.BufferedMCLReader; 17 | import org.monetdb.mcl.io.BufferedMCLWriter; 18 | 19 | /** 20 | * This program demonstrates how the MonetDB JDBC driver can facilitate 21 | * in performing COPY INTO ... FROM STDIN sequences. 22 | * It shows how a data stream via MapiSocket to STDIN can be performed. 23 | * 24 | * @author Fabian Groffen, Martin van Dinther 25 | */ 26 | 27 | public final class SQLcopyinto { 28 | final private static String tablenm = "exampleSQLCopyInto"; 29 | 30 | public static void main(String[] args) throws Exception { 31 | System.out.println("SQLcopyinto started"); 32 | if (args.length == 0) { 33 | System.err.println("Error: missing startup argument: the jdbc connection url !"); 34 | System.err.println("Usage: java -cp monetdb-jdbc-3.0.jre8.jar:. SQLcopyinto \"jdbc:monetdb://localhost:50000/demo?user=monetdb&password=monetdb\""); 35 | System.exit(-1); 36 | } 37 | String jdbc_url = args[0]; 38 | 39 | // request a connection to MonetDB server via the driver manager 40 | Connection conn = DriverManager.getConnection(jdbc_url); 41 | System.out.println("Connected to MonetDB server"); 42 | 43 | Statement stmt = null; 44 | ResultSet rs = null; 45 | try { 46 | stmt = conn.createStatement(); 47 | stmt.execute("CREATE TABLE IF NOT EXISTS " + tablenm + " (id int, val varchar(24))"); 48 | 49 | fillTableUsingCopyIntoSTDIN(conn); 50 | 51 | // check content of the table populated via COPY INTO ... FROM STDIN 52 | System.out.println("Listing uploaded data:"); 53 | rs = stmt.executeQuery("SELECT * FROM " + tablenm); 54 | if (rs != null) { 55 | while (rs.next()) { 56 | System.out.println("Row data: " + rs.getString(1) + ", " + rs.getString(2)); 57 | } 58 | rs.close(); 59 | rs = null; 60 | } 61 | 62 | stmt.execute("DROP TABLE " + tablenm); 63 | 64 | System.out.println("SQLcopyinto completed"); 65 | } catch (SQLException e) { 66 | System.err.println("SQLException: " + e.getMessage()); 67 | } catch (Exception e) { 68 | System.err.println("Exception: " + e.getMessage()); 69 | } finally { 70 | // free resources 71 | if (rs != null) 72 | rs.close(); 73 | if (stmt != null) 74 | stmt.close(); 75 | // close the JDBC connection to MonetDB server 76 | if (conn != null) 77 | conn.close(); 78 | } 79 | } 80 | 81 | public static void fillTableUsingCopyIntoSTDIN(Connection mcon) throws Exception { 82 | System.out.println(); 83 | System.out.println("CopyInto STDIN begin"); 84 | 85 | MapiSocket server = new MapiSocket(); 86 | try { 87 | server.setLanguage("sql"); 88 | 89 | // extract from MonetConnection object the used connection properties 90 | String host = mcon.getClientInfo("host"); 91 | int port = Integer.parseInt(mcon.getClientInfo("port")); 92 | String login = mcon.getClientInfo("user"); 93 | String passw = mcon.getClientInfo("password"); 94 | // System.out.println("host: " + host + " port: " + port + " login: " + login + " passwd: " + passw); 95 | 96 | System.out.println("Before connecting to MonetDB server via MapiSocket"); 97 | List warning = server.connect(host, port, login, passw); 98 | if (warning != null) { 99 | for (Iterator it = warning.iterator(); it.hasNext(); ) { 100 | System.out.println("Warning: " + it.next().toString()); 101 | } 102 | } 103 | System.out.println("Connected to MonetDB server via MapiSocket"); 104 | 105 | BufferedMCLReader mclIn = server.getReader(); 106 | BufferedMCLWriter mclOut = server.getWriter(); 107 | 108 | String error = mclIn.discardRemainder(); 109 | if (error != null) 110 | System.err.println("Received start error: " + error); 111 | 112 | System.out.println("Before sending data to STDIN"); 113 | 114 | // the leading 's' is essential, since it is a protocol marker 115 | // that should not be omitted, likewise the trailing semicolon 116 | mclOut.write('s'); 117 | mclOut.write("COPY INTO " + tablenm + " FROM STDIN USING DELIMITERS ',',E'\\n';"); 118 | mclOut.newLine(); 119 | // now write the row data values as csv data lines to the STDIN stream 120 | for (int i = 0; i < 40; i++) { 121 | mclOut.write("" + i + ",val_" + i); 122 | mclOut.newLine(); 123 | } 124 | 125 | mclOut.writeLine(""); // need this one for synchronisation over flush() 126 | error = mclIn.discardRemainder(); 127 | if (error != null) 128 | System.err.println("Received error: " + error); 129 | 130 | mclOut.writeLine(""); // need this one for synchronisation over flush() 131 | error = mclIn.discardRemainder(); 132 | if (error != null) 133 | System.err.println("Received finish error: " + error); 134 | 135 | System.out.println("Completed sending data via STDIN"); 136 | } catch (Exception e) { 137 | System.err.println("Mapi Exception: " + e.getMessage()); 138 | } finally { 139 | // close MapiSocket connection to MonetDB server 140 | server.close(); 141 | } 142 | 143 | System.out.println("CopyInto STDIN end"); 144 | System.out.println(); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /tests/Test_Cforkbomb.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | import java.sql.*; 14 | import java.util.*; 15 | 16 | public class Test_Cforkbomb { 17 | private static String args[]; 18 | 19 | static class Worker extends Thread { 20 | private int id; 21 | 22 | public Worker(int id) { 23 | this.id = id; 24 | } 25 | 26 | public void run() { 27 | try { 28 | System.out.print("Establishing Connection " + id + "..."); 29 | Connection con = DriverManager.getConnection(args[0]); 30 | System.out.println(" done..."); 31 | 32 | // do something with the connection to test if it works 33 | Statement stmt = con.createStatement(); 34 | ResultSet rs = stmt.executeQuery("SELECT " + id); 35 | if (!rs.next()) { 36 | System.out.println("thread " + id + " got no response from server :("); 37 | } else { 38 | if (rs.getInt(1) == id) { 39 | System.out.println("thread " + id + ": connection ok"); 40 | } else { 41 | System.out.println("thread " + id + ": got garbage: " + rs.getString(1)); 42 | } 43 | } 44 | 45 | con.close(); 46 | } catch (SQLException e) { 47 | System.out.println("thread " + id + " unhappy: " + e.toString()); 48 | } 49 | } 50 | } 51 | 52 | public static void main(String[] args) throws Exception { 53 | Test_Cforkbomb.args = args; 54 | 55 | // just DoS the server full throttle :) 56 | int i; 57 | for (i = 0; i < 200; i++) { 58 | Worker w = new Worker(i); 59 | w.start(); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/Test_Csendthread.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MPL-2.0 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Copyright 2024, 2025 MonetDB Foundation; 9 | * Copyright August 2008 - 2023 MonetDB B.V.; 10 | * Copyright 1997 - July 2008 CWI. 11 | */ 12 | 13 | import java.sql.*; 14 | 15 | public class Test_Csendthread { 16 | public static void main(String[] args) throws Exception { 17 | System.out.println("0. active threads: " + Thread.activeCount()); 18 | 19 | StringBuilder sb = new StringBuilder(); 20 | sb.append("SELECT 1"); 21 | for (int i = 0; i < 256; i++) { 22 | sb.append("-- ADDING DUMMY TEXT AS COMMENT TO MAKE THE QUERY VERY VERY VERY VERY LONG\n"); 23 | } 24 | sb.append(";\n"); 25 | String longQuery = sb.toString(); 26 | 27 | for (int i = 0; i < 10; i++) { 28 | for (int j = 0; j < 10; j++) { 29 | Connection conn = DriverManager.getConnection(args[0]); 30 | try { 31 | Statement st = conn.createStatement(); 32 | st.execute(longQuery); 33 | st.close(); 34 | } finally { 35 | conn.close(); 36 | } 37 | } 38 | System.out.println("1. active threads: " + Thread.activeCount()); 39 | } 40 | System.out.println("2. active threads: " + Thread.activeCount()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/build.properties: -------------------------------------------------------------------------------- 1 | # add a line like this to your build.local.properties to enable 2 | # debugging: 3 | #debug=&debug=true 4 | 5 | # by default debugging is disabled 6 | debug= 7 | 8 | # one can override the connection url as well 9 | #jdbc_url=jdbc:monetdb://localhost:53210/one?user=monetdb&password=monetdb 10 | -------------------------------------------------------------------------------- /tests/build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 40 | 42 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /tests/drop.sql: -------------------------------------------------------------------------------- 1 | -- SPDX-License-Identifier: MPL-2.0 2 | -- 3 | -- This Source Code Form is subject to the terms of the Mozilla Public 4 | -- License, v. 2.0. If a copy of the MPL was not distributed with this 5 | -- file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | -- 7 | -- Copyright 2024, 2025 MonetDB Foundation; 8 | -- Copyright August 2008 - 2023 MonetDB B.V.; 9 | -- Copyright 1997 - July 2008 CWI. 10 | 11 | drop table "foreign"; 12 | drop table allnewtriples; 13 | commit; 14 | -------------------------------------------------------------------------------- /tests/javaspecific.md: -------------------------------------------------------------------------------- 1 | # Java-specific tests 2 | 3 | Test settings that are only in monetdb-java. 4 | 5 | ```test 6 | ONLY jdbc 7 | EXPECT so_timeout=0 8 | SET so_timeout=42 9 | EXPECT so_timeout=42 10 | ACCEPT monetdb://?so_timeout=99 11 | EXPECT so_timeout=99 12 | ``` 13 | 14 | ```test 15 | ONLY jdbc 16 | EXPECT treat_clob_as_varchar=true 17 | SET treat_clob_as_varchar=off 18 | EXPECT treat_clob_as_varchar=false 19 | ACCEPT monetdb://?treat_clob_as_varchar=yes 20 | EXPECT treat_clob_as_varchar=on 21 | ``` 22 | 23 | ```test 24 | ONLY jdbc 25 | EXPECT treat_blob_as_binary=true 26 | SET treat_blob_as_binary=off 27 | EXPECT treat_blob_as_binary=false 28 | ACCEPT monetdb://?treat_blob_as_binary=yes 29 | EXPECT treat_blob_as_binary=on 30 | ``` 31 | 32 | ```test 33 | ONLY jdbc 34 | EXPECT client_info=true 35 | EXPECT client_application= 36 | EXPECT client_remark= 37 | ``` 38 | 39 | ```test 40 | ONLY jdbc 41 | SET client_info=false 42 | SET client_application=myapp 43 | SET client_remark=a remark 44 | EXPECT client_info=false 45 | EXPECT client_application=myapp 46 | EXPECT client_remark=a remark 47 | ``` 48 | -------------------------------------------------------------------------------- /version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # SPDX-License-Identifier: MPL-2.0 4 | # 5 | # This Source Code Form is subject to the terms of the Mozilla Public 6 | # License, v. 2.0. If a copy of the MPL was not distributed with this 7 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | # 9 | # Copyright 2024, 2025 MonetDB Foundation; 10 | # Copyright August 2008 - 2023 MonetDB B.V.; 11 | # Copyright 1997 - July 2008 CWI. 12 | 13 | if [[ -z $1 ]] ; then 14 | echo "Usage: $0 [-w] <(major|minor)=newversion> [...]" 15 | echo "where -w activates actual write of changes" 16 | exit -1 17 | fi 18 | 19 | PROPERTIES='build.properties' 20 | 21 | get_value() { 22 | local tmp=$(grep -E "^$*=" ${PROPERTIES}) 23 | echo ${tmp#*=} 24 | } 25 | 26 | escape_value() { 27 | echo "$*" | sed -e 's/\*/\\*/g' -e 's/\./\\./g' 28 | } 29 | 30 | patch="cat" 31 | 32 | # get rid of the script name 33 | case $1 in 34 | -w) 35 | patch="patch -p0"; 36 | shift 37 | ;; 38 | esac 39 | 40 | CUR_MAJOR=$(eval "get_value 'JDBC_MAJOR'") 41 | CUR_MINOR=$(eval "get_value 'JDBC_MINOR'") 42 | 43 | NEW_MAJOR=${CUR_MAJOR} 44 | NEW_MINOR=${CUR_MINOR} 45 | 46 | ESC_MAJOR=$(escape_value ${CUR_MAJOR}) 47 | ESC_MINOR=$(escape_value ${CUR_MINOR}) 48 | 49 | for param in $* ; do 50 | arg=${param%%=*} 51 | val=${param#*=} 52 | num=$(echo ${val} | grep -E '[0-9]+' -o | head -n1) 53 | case ${arg} in 54 | major) 55 | if [[ -z ${num} ]] ; then 56 | echo "major needs a numeric argument!"; 57 | exit -1 58 | fi 59 | NEW_MAJOR=${num} 60 | ;; 61 | minor) 62 | if [[ -z ${num} ]] ; then 63 | echo "minor needs a numeric argument!"; 64 | exit -1 65 | fi 66 | NEW_MINOR=${num} 67 | ;; 68 | esac 69 | done 70 | 71 | echo "Current version: ${CUR_MAJOR}.${CUR_MINOR}" 72 | echo "New version: ${NEW_MAJOR}.${NEW_MINOR}" 73 | 74 | diff="diff -Naur" 75 | 76 | file="release.txt" 77 | sed \ 78 | -e "s|version ${ESC_MAJOR}\.${ESC_MINOR}|version ${NEW_MAJOR}.${NEW_MINOR}|g" \ 79 | -e "s|JDBC-${ESC_MAJOR}\.${ESC_MINOR}|JDBC-${NEW_MAJOR}.${NEW_MINOR}|g" \ 80 | -e "s|Release date: 20[0-9][0-9]-[01][0-9]-[0-3][0-9]|Release date: `date +%F`|" \ 81 | ${file} | ${diff} ${file} - | ${patch} 82 | 83 | file="build.properties" 84 | sed \ 85 | -e "s|JDBC_MAJOR=${ESC_MAJOR}|JDBC_MAJOR=${NEW_MAJOR}|g" \ 86 | -e "s|JDBC_MINOR=${ESC_MINOR}|JDBC_MINOR=${NEW_MINOR}|g" \ 87 | ${file} | ${diff} ${file} - | ${patch} 88 | 89 | file="pom.xml" 90 | sed \ 91 | -e "/monetdb-jdbc/,/MonetDB JDBC driver/s|${ESC_MAJOR}\.${ESC_MINOR}|${NEW_MAJOR}.${NEW_MINOR}|g" \ 92 | ${file} | ${diff} ${file} - | ${patch} 93 | --------------------------------------------------------------------------------