├── tests ├── __init__.py ├── parts │ ├── test_rowsaffected.py │ ├── test_clientid.py │ ├── test_authentication.py │ ├── test_connectionoptions.py │ ├── test_options.py │ └── test_fields.py ├── test_error.py ├── lib │ ├── test_stringlib.py │ └── test_tracing.py ├── test_hdbclient_compatibility.py ├── dbapi │ ├── test_connection.py │ └── test_module.py ├── test_call_stored.py ├── test_connection.py ├── types │ ├── test_escape.py │ ├── test_meta.py │ ├── test_datetime.py │ ├── test_string.py │ ├── test_int.py │ └── test_geometry.py ├── test_auth.py ├── conftest.py ├── helper.py ├── test_cesu8.py ├── test_transactions.py ├── test_dummy_sql.py ├── test_parts.py ├── test_message.py └── test_cursor.py ├── VERSION ├── pyhdb ├── lib │ ├── __init__.py │ ├── stringlib.py │ └── tracing.py ├── protocol │ ├── __init__.py │ ├── constants │ │ ├── parameter_direction.py │ │ ├── general.py │ │ ├── segment_kinds.py │ │ ├── message_types.py │ │ ├── function_codes.py │ │ ├── __init__.py │ │ ├── part_kinds.py │ │ └── type_codes.py │ ├── message.py │ ├── headers.py │ ├── segments.py │ └── lobs.py ├── exceptions.py ├── compat.py ├── auth.py ├── __init__.py ├── cesu8.py └── connection.py ├── requirements-tests.txt ├── AUTHORS ├── MANIFEST.in ├── .travis.yml ├── tox.ini ├── CHANGES ├── setup.py ├── .gitignore ├── README.rst └── LICENSE /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.3.5.dev 2 | -------------------------------------------------------------------------------- /pyhdb/lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements-tests.txt: -------------------------------------------------------------------------------- 1 | pytest>=3.6.0 2 | mock>=2.0.0 3 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Authors ordered by first contribution. 2 | 3 | Christoph Heer 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include setup.py README.rst MANIFEST.in VERSION AUTHORS CHANGES LICENSE 2 | -------------------------------------------------------------------------------- /pyhdb/protocol/__init__.py: -------------------------------------------------------------------------------- 1 | # The following import is required to register the 'cesu8' codec in Python: 2 | import pyhdb.cesu8 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 4 | - "2.7" 5 | - "3.4" 6 | - "3.5" 7 | - "3.6" 8 | install: 9 | - pip install -U pip setuptools wheel 10 | - pip install -r requirements-tests.txt 11 | - pip install -e . 12 | script: 13 | - pytest -v 14 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py26, py27, pypy, py33, py34, py35 8 | 9 | [testenv] 10 | commands = py.test 11 | deps = 12 | pytest 13 | mock 14 | -------------------------------------------------------------------------------- /pyhdb/protocol/constants/parameter_direction.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | IN = 1 # Parameter direction is IN. 16 | INOUT = 2 # Parameter direction is INOUT 17 | OUT = 4 # Parameter direction is OUT. 18 | -------------------------------------------------------------------------------- /pyhdb/protocol/constants/general.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | MAX_MESSAGE_SIZE = 2**17 16 | MESSAGE_HEADER_SIZE = 32 # this will be verified on 'Message'-class creation 17 | MAX_SEGMENT_SIZE = MAX_MESSAGE_SIZE - MESSAGE_HEADER_SIZE 18 | -------------------------------------------------------------------------------- /pyhdb/protocol/constants/segment_kinds.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Segment kinds 16 | 17 | RESERVED = 0 # reserved for invalid segments, do not use 18 | REQUEST = 1 19 | REPLY = 2 20 | ERROR = 5 # reply segment containing error 21 | -------------------------------------------------------------------------------- /pyhdb/protocol/constants/message_types.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Message types 16 | 17 | # These values are embedded in REQUEST segment headers 18 | 19 | EXECUTEDIRECT = 2 20 | PREPARE = 3 21 | EXECUTE = 13 22 | READLOB = 16 23 | WRITELOB = 17 24 | AUTHENTICATE = 65 25 | CONNECT = 66 26 | COMMIT = 67 27 | ROLLBACK = 68 28 | FETCHNEXT = 71 29 | DISCONNECT = 77 30 | -------------------------------------------------------------------------------- /pyhdb/protocol/constants/function_codes.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Function codes 16 | 17 | # These values are embedded in REPLY segment headers 18 | 19 | DDL = 1 20 | INSERT = 2 21 | UPDATE = 3 22 | DELETE = 4 23 | SELECT = 5 24 | DBPROCEDURECALL = 8 # CALL statement 25 | DBPROCEDURECALLWITHRESULT = 9 # CALL statement returning one or more results 26 | WRITELOB = 15 27 | READLOB = 16 28 | DISCONNECT = 18 29 | 30 | DML = frozenset([INSERT, UPDATE, DELETE]) 31 | -------------------------------------------------------------------------------- /tests/parts/test_rowsaffected.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | from io import BytesIO 16 | from pyhdb.protocol.parts import RowsAffected 17 | 18 | 19 | def test_unpack_one_value(): 20 | values = RowsAffected.unpack_data( 21 | 1, 22 | BytesIO(b"\x01\x00\x00\x00") 23 | ) 24 | assert values == ((1,),) 25 | 26 | 27 | def test_unpack_multiple_values(): 28 | values = RowsAffected.unpack_data( 29 | 3, 30 | BytesIO(b"\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00") 31 | ) 32 | assert values == ((1, 2, 3),) 33 | -------------------------------------------------------------------------------- /tests/parts/test_clientid.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | from io import BytesIO 16 | from pyhdb.protocol.parts import ClientId 17 | from pyhdb.protocol import constants 18 | 19 | 20 | def test_pack_data(): 21 | part = ClientId("bla@example.com") 22 | arguments, payload = part.pack_data(constants.MAX_SEGMENT_SIZE) 23 | assert arguments == 1 24 | assert payload == "bla@example.com".encode('cesu-8') 25 | 26 | 27 | def test_unpack_data(): 28 | client_id = ClientId.unpack_data(1, BytesIO(b"bla@example.com")) 29 | assert client_id == "bla@example.com" 30 | -------------------------------------------------------------------------------- /tests/test_error.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | import pytest 16 | from pyhdb.protocol.message import RequestMessage 17 | from pyhdb.protocol.segments import RequestSegment 18 | from pyhdb.exceptions import DatabaseError 19 | 20 | 21 | @pytest.mark.hanatest 22 | def test_invalid_request(connection): 23 | request = RequestMessage.new( 24 | connection, 25 | RequestSegment(2) 26 | ) 27 | 28 | with pytest.raises(DatabaseError): 29 | connection.send_request(request) 30 | 31 | 32 | @pytest.mark.hanatest 33 | def test_invalid_sql(connection): 34 | cursor = connection.cursor() 35 | 36 | with pytest.raises(DatabaseError): 37 | cursor.execute("SELECT FROM DUMMY") 38 | -------------------------------------------------------------------------------- /pyhdb/protocol/constants/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | DEFAULT_CONNECTION_OPTIONS = { 16 | "connection_id": None, 17 | "complete_array_execution": True, 18 | "client_locale": "en_US", 19 | "supports_large_bulk_operations": None, 20 | "large_number_of_parameters_support": None, 21 | "system_id": None, 22 | "select_for_update_supported": False, 23 | "client_distribution_mode": 0, 24 | "engine_data_format_version": None, 25 | "distribution_protocol_version": 0, 26 | "split_batch_commands": True, 27 | "use_transaction_flags_only": None, 28 | "row_and_column_optimized_format": None, 29 | "ignore_unknown_parts": None, 30 | "data_format_version": 1, 31 | "data_format_version2": 1 32 | } 33 | 34 | from pyhdb.protocol.constants.general import MAX_MESSAGE_SIZE, MAX_SEGMENT_SIZE 35 | -------------------------------------------------------------------------------- /tests/lib/test_stringlib.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | from pyhdb.lib.stringlib import humanhexlify, allhexlify, dehexlify 16 | 17 | 18 | def test_humanhexlify(): 19 | """Test plain humanhexlify function without shortening""" 20 | b = b'\x01\x62\x70\x00\xff' 21 | assert humanhexlify(b) == b'01 62 70 00 ff' 22 | 23 | 24 | def test_humanhexlify_shorten(): 25 | """Test plain humanhexlify function with shortening to 3 bytes""" 26 | b = b'\x01\x62\x70\x00\xff' 27 | assert humanhexlify(b, n=3) == b'01 62 70 ...' 28 | 29 | 30 | def test_allhexlify(): 31 | """Test that ALL byte chars are converted into hex values""" 32 | b = b'ab\x04ce' 33 | assert allhexlify(b) == b'\\x61\\x62\\x04\\x63\\x65' 34 | 35 | 36 | def test_dehexlify(): 37 | """Test reverting of humanhexlify""" 38 | b = '61 62 04 63 65' 39 | assert dehexlify(b) == b'ab\x04ce' 40 | -------------------------------------------------------------------------------- /tests/test_hdbclient_compatibility.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | import pytest 16 | from pyhdb.connection import Connection 17 | 18 | 19 | def test_getautocommit(): 20 | connection = Connection("localhost", 30015, "Fuu", "Bar") 21 | assert not connection.getautocommit() 22 | 23 | connection.autocommit = True 24 | assert connection.getautocommit() 25 | 26 | 27 | def test_setautocommit(): 28 | connection = Connection("localhost", 30015, "Fuu", "Bar") 29 | 30 | connection.setautocommit(False) 31 | assert not connection.autocommit 32 | 33 | connection.setautocommit(True) 34 | assert connection.autocommit 35 | 36 | 37 | @pytest.mark.hanatest 38 | def test_isconnected(hana_system): 39 | connection = Connection(*hana_system) 40 | assert not connection.isconnected() 41 | 42 | connection.connect() 43 | assert connection.isconnected() 44 | 45 | connection.close() 46 | assert not connection.isconnected() 47 | -------------------------------------------------------------------------------- /tests/dbapi/test_connection.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | import pytest 16 | import pyhdb 17 | 18 | 19 | # Test for DBAPI 2.0 compliance 20 | @pytest.mark.hanatest 21 | def test_fixture_connection(connection): 22 | # Smoke test of the connection fixture 23 | pass 24 | 25 | 26 | @pytest.mark.hanatest 27 | def test_commit(connection): 28 | connection.commit() 29 | 30 | 31 | @pytest.mark.hanatest 32 | def test_rollback(connection): 33 | connection.rollback() 34 | 35 | 36 | @pytest.mark.hanatest 37 | def test_cursor(connection): 38 | cursor = connection.cursor() 39 | assert isinstance(cursor, pyhdb.cursor.Cursor) 40 | 41 | 42 | @pytest.mark.hanatest 43 | @pytest.mark.parametrize("method", [ 44 | 'close', 45 | 'commit', 46 | 'rollback', 47 | 'cursor', 48 | ]) 49 | def test_method_raises_error_after_close(hana_system, method): 50 | connection = pyhdb.connect(*hana_system) 51 | connection.close() 52 | 53 | with pytest.raises(pyhdb.Error): 54 | getattr(connection, method)() 55 | -------------------------------------------------------------------------------- /pyhdb/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Enable absolute import, otherwise the 'exceptions' module of stdlib will not be found 16 | from __future__ import absolute_import 17 | 18 | 19 | class Error(Exception): 20 | pass 21 | 22 | class Warning(Warning): 23 | pass 24 | 25 | class InterfaceError(Error): 26 | pass 27 | 28 | 29 | class DatabaseError(Error): 30 | 31 | def __init__(self, message, code=None): 32 | super(DatabaseError, self).__init__(message) 33 | self.code = code 34 | 35 | 36 | class InternalError(DatabaseError): 37 | pass 38 | 39 | 40 | class OperationalError(DatabaseError): 41 | pass 42 | 43 | 44 | class ConnectionTimedOutError(OperationalError): 45 | 46 | def __init__(self, message=None): 47 | super(ConnectionTimedOutError, self).__init__(message) 48 | 49 | 50 | class ProgrammingError(DatabaseError): 51 | pass 52 | 53 | 54 | class IntegrityError(DatabaseError): 55 | pass 56 | 57 | 58 | class DataError(DatabaseError): 59 | pass 60 | 61 | 62 | class NotSupportedError(DatabaseError): 63 | pass 64 | -------------------------------------------------------------------------------- /tests/dbapi/test_module.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | import pytest 16 | 17 | import pyhdb 18 | 19 | 20 | # Module globals defined by DBAPI 2.0 21 | def test_apilevel(): 22 | assert pyhdb.apilevel == "2.0" 23 | 24 | 25 | def test_threadsafety(): 26 | assert pyhdb.threadsafety == 2 27 | 28 | 29 | def test_paramstyle(): 30 | assert pyhdb.paramstyle == "numeric" 31 | 32 | 33 | def test_exceptions(): 34 | assert issubclass(pyhdb.Warning, Exception) 35 | assert issubclass(pyhdb.Error, Exception) 36 | assert issubclass(pyhdb.InterfaceError, pyhdb.Error) 37 | assert issubclass(pyhdb.DatabaseError, pyhdb.Error) 38 | assert issubclass(pyhdb.OperationalError, pyhdb.Error) 39 | assert issubclass(pyhdb.IntegrityError, pyhdb.Error) 40 | assert issubclass(pyhdb.InternalError, pyhdb.Error) 41 | assert issubclass(pyhdb.ProgrammingError, pyhdb.Error) 42 | assert issubclass(pyhdb.NotSupportedError, pyhdb.Error) 43 | 44 | 45 | @pytest.mark.hanatest 46 | def test_connect_constructors(hana_system): 47 | connection = pyhdb.connect(*hana_system) 48 | assert isinstance(connection, pyhdb.connection.Connection) 49 | connection.close() 50 | -------------------------------------------------------------------------------- /pyhdb/protocol/constants/part_kinds.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Part kinds 16 | 17 | COMMAND = 3 # SQL Command Data 18 | RESULTSET = 5 # Tabular result set data 19 | ERROR = 6 # Error information 20 | STATEMENTID = 10 # Prepared statement identifier 21 | ROWSAFFECTED = 12 # Number of affected rows 22 | RESULTSETID = 13 # Result set identifier 23 | TOPOLOGYINFORMATION = 15 # Topoloygy information 24 | READLOBREQUEST = 17 # Request for reading (part of) a lob 25 | READLOBREPLY = 18 # Reply of request for reading (part of) a lob 26 | WRITELOBREQUEST = 28 # Request of data of WRITELOB message 27 | WRITELOBREPLY = 30 # Reply data of WRITELOB message 28 | PARAMETERS = 32 # Parameter data 29 | AUTHENTICATION = 33 # Authentication data 30 | CLIENTID = 35 # (undocumented) client id 31 | STATEMENTCONTEXT = 39 # Statement visibility context 32 | OUTPUTPARAMETERS = 41 # Output parameter data 33 | CONNECTOPTIONS = 42 # Connect options 34 | FETCHSIZE = 45 # Numbers of rows to fetch 35 | PARAMETERMETADATA = 47 # Parameter metadata (type and length information) 36 | RESULTSETMETADATA = 48 # Result set metadata (type, length, and name information) 37 | TRANSACTIONFLAGS = 64 # Transaction handling flags 38 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 0.3.5.dev 5 | --------- 6 | - Fixed various problems in decoding and encoding of CESU-8 (#102) 7 | 8 | 0.3.4 9 | ----- 10 | - Fixed parameters tuple access with unicode index (#98) 11 | - Ignore part 47 PARAMETERMETADATA in _handle_upsert (#89) 12 | 13 | 0.3.3 14 | ----- 15 | - Truncate (instead of round) microseconds to milliseconds in time or timestamp columns. 16 | 17 | 0.3.2 18 | ----- 19 | - Accept PARAMETERMETADATA part while handling select response 20 | - Allow Decimal in prepared statements 21 | 22 | 0.3.1 23 | ----- 24 | - Raise IntegrityError on unique constraint violation 25 | 26 | 0.3.0 27 | ----- 28 | - Added LOB_WRITE requests allowing to insert or update LOBs of arbitrary size 29 | - Added support of SELECT FOR UPDATE statements 30 | - Added support of Geometry types 31 | - Added support for procedures with resultsets 32 | - Fixed and improved Real, BigInt, Time and Date types 33 | - Code cleanup and refactoring 34 | - Renamed logger form receive to pyhdb 35 | - Reactivated unit tests which were deactivated by accident 36 | - Added pyhdb.connect.from_ini() to connect to db with parameters from properly formatted ini file 37 | 38 | 0.2.4 39 | ----- 40 | - added support for packing timestamps into payload 41 | - added support for casting integers and floats provided in string format for packing into payload 42 | - added support for casting floats and integers into strings where string is requested for payload 43 | 44 | 0.2.3 45 | ----- 46 | - fixed nasty bug when looping over long list of result rows from select statement 47 | 48 | 0.2.1 49 | ----- 50 | - CLOBs are now using cStringIO as underlying container to hold data as 'str' and not unicode (Python 2.x) 51 | - Updated README (added basic information about LOBs) 52 | 53 | 0.2.0 54 | ----- 55 | - Support for BLOB, CLOB, and NCLOB 56 | - Lots of code refactoring and PEP8 cleanup 57 | 58 | 0.1.0 59 | ----- 60 | - First release version 61 | -------------------------------------------------------------------------------- /pyhdb/lib/stringlib.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | import re 16 | import binascii 17 | 18 | 19 | def allhexlify(data): 20 | """Hexlify given data into a string representation with hex values for all chars 21 | Input like 22 | 'ab\x04ce' 23 | becomes 24 | '\x61\x62\x04\x63\x65' 25 | """ 26 | hx = binascii.hexlify(data) 27 | return b''.join([b'\\x' + o for o in re.findall(b'..', hx)]) 28 | 29 | 30 | def humanhexlify(data, n=-1): 31 | """Hexlify given data with 1 space char btw hex values for easier reading for humans 32 | :param data: binary data to hexlify 33 | :param n: If n is a positive integer then shorten the output of this function to n hexlified bytes. 34 | 35 | Input like 36 | 'ab\x04ce' 37 | becomes 38 | '61 62 04 63 65' 39 | 40 | With n=3 input like 41 | data='ab\x04ce', n=3 42 | becomes 43 | '61 62 04 ...' 44 | """ 45 | tail = b' ...' if 0 < n < len(data) else b'' 46 | if tail: 47 | data = data[:n] 48 | hx = binascii.hexlify(data) 49 | return b' '.join(re.findall(b'..', hx)) + tail 50 | 51 | 52 | def dehexlify(hx): 53 | """Revert human hexlification - remove white spaces from hex string and convert into real values 54 | Input like 55 | '61 62 04 63 65' 56 | becomes 57 | 'ab\x04ce' 58 | """ 59 | return binascii.unhexlify(hx.replace(' ', '')) 60 | -------------------------------------------------------------------------------- /pyhdb/compat.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import sys 16 | 17 | PY2 = sys.version_info[0] == 2 18 | PY26 = PY2 and sys.version_info[1] == 6 19 | PY3 = sys.version_info[0] == 3 20 | 21 | if PY2: 22 | text_type = unicode 23 | byte_type = bytearray 24 | string_types = (str, unicode) 25 | int_types = (int, long) 26 | unichr = unichr 27 | iter_range = xrange 28 | import ConfigParser as configparser 29 | from itertools import izip 30 | else: 31 | text_type = str 32 | byte_type = bytes 33 | string_types = (str,) 34 | int_types = (int,) 35 | unichr = chr 36 | iter_range = range 37 | import configparser 38 | izip = zip 39 | 40 | # workaround for 'narrow' Python builds 41 | if sys.maxunicode <= 65535: 42 | unichr = lambda n: ('\\U%08x' % n).decode('unicode-escape') 43 | 44 | 45 | def with_metaclass(meta, *bases): 46 | """ 47 | Function from jinja2/_compat.py. 48 | Author: Armin Ronacher 49 | License: BSD. 50 | """ 51 | class metaclass(meta): 52 | __call__ = type.__call__ 53 | __init__ = type.__init__ 54 | def __new__(cls, name, this_bases, d): 55 | if this_bases is None: 56 | return type.__new__(cls, name, (), d) 57 | return meta(name, bases, d) 58 | return metaclass('temporary_class', None, {}) 59 | 60 | 61 | def is_text(obj): 62 | return isinstance(obj, text_type) 63 | -------------------------------------------------------------------------------- /tests/test_call_stored.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2014, 2015 SAP SE. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http: //www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 14 | # either express or implied. See the License for the specific 15 | # language governing permissions and limitations under the License. 16 | 17 | import pytest 18 | 19 | import tests.helper 20 | 21 | from tests.helper import procedure_add2_fixture, procedure_with_result_fixture 22 | 23 | # ############################################################################################################# 24 | # Basic Stored Procedure test 25 | # ############################################################################################################# 26 | 27 | # ### One or more scalar parameters, OUT or INOUT 28 | @pytest.mark.hanatest 29 | def test_PROC_ADD2(connection, procedure_add2_fixture): 30 | cursor = connection.cursor() 31 | 32 | sql_to_prepare = 'call PYHDB_PROC_ADD2 (?, ?, ?, ?)' 33 | params = [1, 2, None, None] 34 | params = {'A':2, 'B':5, 'C':None, 'D': None} 35 | psid = cursor.prepare(sql_to_prepare) 36 | ps = cursor.get_prepared_statement(psid) 37 | cursor.execute_prepared(ps, [params]) 38 | result = cursor.fetchall() 39 | assert result == [(7, 'A')] 40 | 41 | @pytest.mark.hanatest 42 | def test_proc_with_results(connection, procedure_with_result_fixture): 43 | cursor = connection.cursor() 44 | 45 | # prepare call 46 | psid = cursor.prepare("CALL PYHDB_PROC_WITH_RESULT(?)") 47 | ps = cursor.get_prepared_statement(psid) 48 | 49 | # execute prepared statement 50 | cursor.execute_prepared(ps, [{'OUTVAR': 0}]) 51 | result = cursor.fetchall() 52 | 53 | assert result == [(2015,)] 54 | 55 | cursor.close() 56 | -------------------------------------------------------------------------------- /tests/test_connection.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | # Test additional features of pyhdb.Connection 16 | 17 | import os 18 | import pytest 19 | 20 | from pyhdb.connection import Connection 21 | import pyhdb 22 | 23 | 24 | @pytest.mark.hanatest 25 | def test_initial_timeout(connection): 26 | assert connection.timeout is None 27 | assert connection._timeout is None 28 | assert connection._socket.gettimeout() is None 29 | 30 | 31 | @pytest.mark.hanatest 32 | def test_set_timeout_in_init(hana_system): 33 | connection = Connection(*hana_system, timeout=10) 34 | assert connection.timeout == 10 35 | assert connection._timeout == 10 36 | 37 | 38 | @pytest.mark.hanatest 39 | def test_socket_use_init_timeout(hana_system): 40 | connection = Connection(*hana_system, timeout=10) 41 | connection.connect() 42 | 43 | assert connection._socket.gettimeout() == 10 44 | connection.close() 45 | 46 | 47 | @pytest.mark.hanatest 48 | def test_set_timeout_update_socket_setting(connection): 49 | assert connection.timeout is None 50 | 51 | connection.timeout = 10 52 | assert connection.timeout == 10 53 | assert connection._socket.gettimeout() == 10 54 | 55 | 56 | def test_set_timeout_without_socket(): 57 | connection = Connection("localhost", 30015, "Fuu", "Bar") 58 | connection.timeout = 10 59 | assert connection.timeout == 10 60 | 61 | 62 | def test_make_connection_from_pytest_ini(): 63 | if not os.path.isfile('pytest.ini'): 64 | pytest.skip("Requires pytest.ini file") 65 | connection = pyhdb.connect.from_ini('pytest.ini') 66 | -------------------------------------------------------------------------------- /tests/lib/test_tracing.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | import pyhdb 16 | from pyhdb.protocol.message import RequestMessage 17 | from pyhdb.protocol.segments import RequestSegment 18 | from pyhdb.protocol.parts import Command 19 | from pyhdb.protocol.constants import message_types 20 | from pyhdb.protocol.headers import MessageHeader 21 | from pyhdb.lib.tracing import trace 22 | 23 | TRACE_MSG = '''RequestMessage = { 24 | header = [ 25 | session_id = 5, 26 | packet_count = 3, 27 | payload_length = 500, 28 | varpartsize = 500, 29 | num_segments = 1, 30 | packet_options = 0 31 | ], 32 | segments = [ 33 | RequestSegment = { 34 | header = None, 35 | parts = [ 36 | Command = { 37 | header = None, 38 | trace_header = '', 39 | trace_payload = '', 40 | sql_statement = 'select * from dummy' 41 | } 42 | ] 43 | } 44 | ] 45 | }''' 46 | 47 | 48 | def test_tracing_output(): 49 | msg_header = MessageHeader(session_id=5, packet_count=3, payload_length=500, varpartsize=500, 50 | num_segments=1, packet_options=0) 51 | request = RequestMessage( 52 | session_id=msg_header.session_id, 53 | packet_count=msg_header.packet_count, 54 | segments=RequestSegment( 55 | message_types.EXECUTE, 56 | Command('select * from dummy') 57 | ), 58 | header=msg_header 59 | ) 60 | 61 | pyhdb.tracing = True 62 | try: 63 | trace_msg = trace(request) 64 | finally: 65 | pyhdb.tracing = False 66 | 67 | assert trace_msg == TRACE_MSG 68 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | import os 16 | import codecs 17 | from setuptools import setup, find_packages 18 | 19 | source_location = os.path.abspath(os.path.dirname(__file__)) 20 | 21 | 22 | def get_version(): 23 | with open(os.path.join(source_location, "VERSION")) as version: 24 | return version.readline().strip() 25 | 26 | 27 | def get_long_description(): 28 | with codecs.open(os.path.join(source_location, "README.rst"), 'r', 'utf-8') as readme: 29 | return readme.read() 30 | 31 | setup( 32 | name="pyhdb", 33 | version=get_version(), 34 | license="Apache License Version 2.0", 35 | url="https://github.com/SAP/pyhdb", 36 | author="Christoph Heer", 37 | author_email="christoph.heer@sap.com", 38 | description="SAP HANA Database Client for Python", 39 | include_package_data=True, 40 | long_description=get_long_description(), 41 | packages=find_packages(exclude=("tests", "tests.*",)), 42 | zip_safe=False, 43 | classifiers=[ # http://pypi.python.org/pypi?%3Aaction=list_classifiers 44 | 'Development Status :: 4 - Beta', 45 | 'Intended Audience :: Developers', 46 | 'License :: OSI Approved :: Apache Software License', 47 | 'Operating System :: OS Independent', 48 | 'Programming Language :: Python :: 2.7', 49 | 'Programming Language :: Python :: 3.3', 50 | 'Programming Language :: Python :: 3.4', 51 | 'Programming Language :: Python :: 3.5', 52 | 'Programming Language :: Python :: 3.6', 53 | 'Programming Language :: Python :: Implementation :: PyPy', 54 | 'Programming Language :: SQL', 55 | 'Topic :: Database', 56 | 'Topic :: Database :: Front-Ends', 57 | 'Topic :: Software Development', 58 | 'Topic :: Software Development :: Libraries :: Python Modules', 59 | ] 60 | ) 61 | -------------------------------------------------------------------------------- /tests/types/test_escape.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2014, 2015 SAP SE. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http: //www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 14 | # either express or implied. See the License for the specific 15 | # language governing permissions and limitations under the License. 16 | 17 | import pytest 18 | from pyhdb.protocol import types 19 | from pyhdb.exceptions import InterfaceError 20 | 21 | 22 | def test_escape_unsupported_type(): 23 | with pytest.raises(InterfaceError): 24 | types.escape(lambda *args: args) 25 | 26 | 27 | def test_escape_simple_string(): 28 | text = "Hello World" 29 | assert types.escape(text) == "'Hello World'" 30 | 31 | 32 | def test_escape_simple_unicode(): 33 | text = u"Hello World" 34 | assert types.escape(text) == u"'Hello World'" 35 | 36 | 37 | def test_escape_string_with_apostrophe(): 38 | text = "'Hello' \"World\"" 39 | assert types.escape(text) == "'''Hello'' \"World\"'" 40 | 41 | 42 | def test_escape_unicode_with_apostrophe(): 43 | text = u"'Hüllö' \"Wörldß\"" 44 | assert types.escape(text) == u"'''Hüllö'' \"Wörldß\"'" 45 | 46 | 47 | def test_escape_list(): 48 | assert types.escape(("a", "b")) == "('a', 'b')" 49 | 50 | 51 | def test_escape_list_with_apostrophe(): 52 | assert types.escape(("a'", "'b")) == "('a''', '''b')" 53 | 54 | 55 | def test_escape_values_from_list(): 56 | arguments = ["'Hello'", "World"] 57 | assert types.escape_values(arguments) == ("'''Hello'''", '\'World\'') 58 | 59 | 60 | def test_escape_values_from_tuple(): 61 | arguments = ("'Hello'", "World") 62 | assert types.escape_values(arguments) == ("'''Hello'''", '\'World\'') 63 | 64 | 65 | def test_escape_values_from_dict(): 66 | arguments = { 67 | "verb": "'Hello'", 68 | "to": "World" 69 | } 70 | assert types.escape_values(arguments) == { 71 | 'verb': "'''Hello'''", 72 | 'to': "'World'" 73 | } 74 | 75 | 76 | def test_escape_values_raises_exception_with_wrong_type(): 77 | with pytest.raises(InterfaceError): 78 | types.escape_values(None) 79 | 80 | 81 | def test_escape_None(): 82 | types.escape(None) == "NULL" 83 | -------------------------------------------------------------------------------- /tests/test_auth.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | import pytest 16 | 17 | from pyhdb.auth import AuthManager 18 | 19 | 20 | @pytest.fixture 21 | def auth_manager(): 22 | manager = AuthManager(None, "TestUser", "secret") 23 | manager.client_key = b"\xed\xbd\x7c\xc8\xb2\xf2\x64\x89\xd6\x5a\x7c\xd5" \ 24 | b"\x1e\x27\xf2\xe7\x3f\xca\x22\x7d\x1a\xb6\xaa\xfc" \ 25 | b"\xac\x0f\x42\x8c\xa4\xd8\xe1\x0c\x19\xe3\xe3\x8f" \ 26 | b"\x3a\xac\x51\x07\x5e\x67\xbb\xe5\x2f\xdb\x61\x03" \ 27 | b"\xa7\xc3\x4c\x8a\x70\x90\x8e\xd5\xbe\x0b\x35\x42" \ 28 | b"\x70\x5f\x73\x8c" 29 | return manager 30 | 31 | 32 | class TestSCRAMSHA256(object): 33 | 34 | # Test disabled: 35 | # Init request is not accessable anymore 36 | # 37 | # def test_init_request(self, auth_manager): 38 | # request = auth_manager.get_initial_request() 39 | # assert isinstance(request, RequestSegment) 40 | # assert request.message_type == message_types.AUTHENTICATE 41 | # assert len(request.parts) == 1 42 | # 43 | # part = request.parts[0] 44 | # assert isinstance(part, Part) 45 | # assert part.kind == Authentication.kind 46 | # assert part.user == "TestUser" 47 | # assert part.methods == { 48 | # b"SCRAMSHA256": auth_manager.client_key 49 | # } 50 | 51 | def test_calculate_client_proof(self, auth_manager): 52 | salt = b"\x80\x96\x4f\xa8\x54\x28\xae\x3a\x81\xac" \ 53 | b"\xd3\xe6\x86\xa2\x79\x33" 54 | server_key = b"\x41\x06\x51\x50\x11\x7e\x45\x5f\xec\x2f\x03\xf6" \ 55 | b"\xf4\x7c\x19\xd4\x05\xad\xe5\x0d\xd6\x57\x31\xdc" \ 56 | b"\x0f\xb3\xf7\x95\x4d\xb6\x2c\x8a\xa6\x7a\x7e\x82" \ 57 | b"\x5e\x13\x00\xbe\xe9\x75\xe7\x45\x18\x23\x8c\x9a" 58 | 59 | client_proof = auth_manager.calculate_client_proof( 60 | [salt], server_key 61 | ) 62 | assert client_proof == \ 63 | b"\x00\x01\x20\xe4\x7d\x8f\x24\x48\x55\xb9\x2d\xc9\x66\x39\x5d" \ 64 | b"\x0d\x28\x25\x47\xb5\x4d\xfd\x09\x61\x4d\x44\x37\x4d\xf9\x4f" \ 65 | b"\x29\x3c\x1a\x02\x0e" 66 | -------------------------------------------------------------------------------- /tests/types/test_meta.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | import pytest 16 | from pyhdb.protocol import types 17 | from pyhdb.exceptions import InterfaceError 18 | 19 | 20 | def test_automated_mapping_by_type_code(): 21 | class DummyType(types.Type): 22 | type_code = 127 23 | 24 | assert types.by_type_code[127] == DummyType 25 | assert DummyType not in types.by_python_type.values() 26 | 27 | 28 | def test_automated_mapping_by_multiple_type_code(): 29 | class DummyType(types.Type): 30 | type_code = (126, 127) 31 | 32 | assert types.by_type_code[126] == DummyType 33 | assert types.by_type_code[127] == DummyType 34 | assert DummyType not in types.by_python_type.values() 35 | 36 | 37 | def test_invalid_automated_mapping_by_type_code(): 38 | with pytest.raises(InterfaceError): 39 | class DummyType(types.Type): 40 | type_code = 999 41 | 42 | 43 | def test_automated_mapping_by_python_type(): 44 | class DummyType(types.Type): 45 | python_type = None 46 | 47 | assert types.by_python_type[None] == DummyType 48 | assert DummyType not in types.by_type_code.values() 49 | 50 | 51 | def test_automated_mapping_by_multiple_python_type(): 52 | class DummyType(types.Type): 53 | python_type = (int, None) 54 | 55 | assert types.by_python_type[int] == DummyType 56 | assert types.by_python_type[None] == DummyType 57 | assert DummyType not in types.by_type_code.values() 58 | 59 | 60 | def test_type_mapping_is_a_weakref(): 61 | class DummyType(types.Type): 62 | type_code = 125 63 | python_type = int 64 | 65 | assert types.by_type_code[125] == DummyType 66 | assert types.by_python_type[int] == DummyType 67 | 68 | del DummyType 69 | import gc 70 | gc.collect() 71 | 72 | assert 125 not in types.by_type_code 73 | assert int not in types.by_python_type 74 | 75 | 76 | def test_all_types_with_code_has_method_from_resultset(): 77 | for typ in types.by_type_code.values(): 78 | assert hasattr(typ, "from_resultset") 79 | assert callable(typ.from_resultset) 80 | 81 | 82 | def test_all_types_with_python_type_has_method_to_sql(): 83 | for typ in types.by_python_type.values(): 84 | assert hasattr(typ, "to_sql") 85 | assert callable(typ.to_sql) 86 | -------------------------------------------------------------------------------- /tests/parts/test_authentication.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | from io import BytesIO 16 | from pyhdb.protocol.parts import Authentication 17 | from pyhdb.protocol import constants 18 | 19 | def test_pack_data(): 20 | part = Authentication( 21 | "TestUser", 22 | { 23 | "SCRAMSHA256": b"\xed\xbd\x7c\xc8\xb2\xf2\x64\x89\xd6\x5a\x7c" 24 | b"\xd5\x1e\x27\xf2\xe7\x3f\xca\x22\x7d\x1a\xb6" 25 | b"\xaa\xfc\xac\x0f\x42\x8c\xa4\xd8\xe1\x0c\x19" 26 | b"\xe3\xe3\x8f\x3a\xac\x51\x07\x5e\x67\xbb\xe5" 27 | b"\x2f\xdb\x61\x03\xa7\xc3\x4c\x8a\x70\x90\x8e" 28 | b"\xd5\xbe\x0b\x35\x42\x70\x5f\x73\x8c" 29 | } 30 | ) 31 | 32 | arguments, payload = part.pack_data(constants.MAX_SEGMENT_SIZE) 33 | assert payload == \ 34 | b"\x03\x00\x08\x54\x65\x73\x74\x55\x73\x65\x72\x0b\x53\x43\x52\x41" \ 35 | b"\x4d\x53\x48\x41\x32\x35\x36\x40\xed\xbd\x7c\xc8\xb2\xf2\x64\x89" \ 36 | b"\xd6\x5a\x7c\xd5\x1e\x27\xf2\xe7\x3f\xca\x22\x7d\x1a\xb6\xaa\xfc" \ 37 | b"\xac\x0f\x42\x8c\xa4\xd8\xe1\x0c\x19\xe3\xe3\x8f\x3a\xac\x51\x07" \ 38 | b"\x5e\x67\xbb\xe5\x2f\xdb\x61\x03\xa7\xc3\x4c\x8a\x70\x90\x8e\xd5" \ 39 | b"\xbe\x0b\x35\x42\x70\x5f\x73\x8c" 40 | 41 | 42 | def test_unpack_data(): 43 | packed = BytesIO( 44 | b"\x02\x00\x0b\x53\x43\x52\x41\x4d\x53\x48\x41\x32\x35\x36\x44\x02" 45 | b"\x00\x10\x19\x6e\xa8\xb1\x8d\x51\x66\xe6\xec\x17\x38\xd9\xff\x49" 46 | b"\x02\x83\x30\x23\x06\xa3\x7a\x72\xd4\xfd\x73\x69\xd9\x9b\x2d\xd2" 47 | b"\x6e\xad\xe3\x89\x57\x06\x6e\xa1\x21\x85\x7f\x18\x63\x67\xe4\x9b" 48 | b"\x75\x28\x96\xe0\x3d\x1f\x56\xca\x86\x85\x8c\x5f\xf5\x27\xc3\x18" 49 | b"\x88\x1e\x8c\x00\x00\x00\x00\x00" 50 | ) 51 | 52 | user, methods = Authentication.unpack_data(1, packed) 53 | 54 | assert user is None 55 | assert b"SCRAMSHA256" in methods 56 | assert methods[b"SCRAMSHA256"] == \ 57 | b"\x02\x00\x10\x19\x6e\xa8\xb1\x8d\x51\x66\xe6\xec\x17\x38\xd9\xff" \ 58 | b"\x49\x02\x83\x30\x23\x06\xa3\x7a\x72\xd4\xfd\x73\x69\xd9\x9b\x2d" \ 59 | b"\xd2\x6e\xad\xe3\x89\x57\x06\x6e\xa1\x21\x85\x7f\x18\x63\x67\xe4" \ 60 | b"\x9b\x75\x28\x96\xe0\x3d\x1f\x56\xca\x86\x85\x8c\x5f\xf5\x27\xc3" \ 61 | b"\x18\x88\x1e\x8c" 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by http://www.gitignore.io 2 | 3 | #!! ERROR: Python is undefined. Use list command to see defined gitignore types !!# 4 | 5 | ### SAP internal test ### 6 | saptest 7 | 8 | 9 | ### Python ### 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | 14 | # C extensions 15 | *.so 16 | 17 | # Distribution / packaging 18 | .Python 19 | env/ 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | eggs/ 24 | lib64/ 25 | sdist/ 26 | var/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | .pytest_cache 41 | nosetests.xml 42 | coverage.xml 43 | 44 | # Translations 45 | *.mo 46 | *.pot 47 | 48 | # Django stuff: 49 | *.log 50 | 51 | # Sphinx documentation 52 | docs/_build/ 53 | 54 | # Test configuration (e.g. login data of HANA systems) 55 | pytest.ini 56 | 57 | ### Linux ### 58 | *~ 59 | 60 | # KDE directory preferences 61 | .directory 62 | 63 | 64 | ### OSX ### 65 | .DS_Store 66 | .AppleDouble 67 | .LSOverride 68 | 69 | # Icon must end with two \r 70 | Icon 71 | 72 | 73 | # Thumbnails 74 | ._* 75 | 76 | # Files that might appear on external disk 77 | .Spotlight-V100 78 | .Trashes 79 | 80 | # Directories potentially created on remote AFP share 81 | .AppleDB 82 | .AppleDesktop 83 | Network Trash Folder 84 | Temporary Items 85 | .apdisk 86 | 87 | 88 | ### Windows ### 89 | # Windows image file caches 90 | Thumbs.db 91 | ehthumbs.db 92 | 93 | # Folder config file 94 | Desktop.ini 95 | 96 | # Recycle Bin used on file shares 97 | $RECYCLE.BIN/ 98 | 99 | # Windows Installer files 100 | *.cab 101 | *.msi 102 | *.msm 103 | *.msp 104 | 105 | 106 | ### PyCharm ### 107 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 108 | 109 | ## Directory-based project format 110 | .idea/ 111 | # if you remove the above rule, at least ignore user-specific stuff: 112 | # .idea/workspace.xml 113 | # .idea/tasks.xml 114 | # and these sensitive or high-churn files: 115 | # .idea/dataSources.ids 116 | # .idea/dataSources.xml 117 | # .idea/sqlDataSources.xml 118 | # .idea/dynamic.xml 119 | 120 | ## File-based project format 121 | *.ipr 122 | *.iml 123 | *.iws 124 | 125 | ## Additional for IntelliJ 126 | out/ 127 | 128 | # generated by mpeltonen/sbt-idea plugin 129 | .idea_modules/ 130 | 131 | # generated by JIRA plugin 132 | atlassian-ide-plugin.xml 133 | 134 | # generated by Crashlytics plugin (for Android Studio and Intellij) 135 | com_crashlytics_export_strings.xml 136 | 137 | 138 | ### SublimeText ### 139 | # workspace files are user-specific 140 | *.sublime-workspace 141 | 142 | # project files should be checked into the repository, unless a significant 143 | # proportion of contributors will probably not be using SublimeText 144 | # *.sublime-project 145 | 146 | #sftp configuration file 147 | sftp-config.json 148 | 149 | # virtualfish 150 | .venv 151 | .vscode -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | from collections import namedtuple 16 | 17 | import pytest 18 | import pyhdb 19 | 20 | HANASystem = namedtuple( 21 | 'HANASystem', ['host', 'port', 'user', 'password'] 22 | ) 23 | 24 | 25 | def _get_option(config, key): 26 | return config.getoption(key) or config.inicfg.get(key) 27 | 28 | 29 | @pytest.fixture(scope="session") 30 | def hana_system(): 31 | host = _get_option(pytest.config, 'hana_host') 32 | port = _get_option(pytest.config, 'hana_port') or 30015 33 | user = _get_option(pytest.config, 'hana_user') 34 | password = _get_option(pytest.config, 'hana_password') 35 | return HANASystem(host, port, user, password) 36 | 37 | 38 | @pytest.fixture() 39 | def connection(request, hana_system): 40 | connection = pyhdb.connect(*hana_system) 41 | 42 | def _close(): 43 | connection.close() 44 | 45 | request.addfinalizer(_close) 46 | return connection 47 | 48 | 49 | def pytest_configure(config): 50 | config.addinivalue_line( 51 | "markers", 52 | "hanatest: mark test to run only with SAP HANA system" 53 | ) 54 | 55 | 56 | def pytest_addoption(parser): 57 | parser.addoption( 58 | "--hana-host", 59 | help="Address of SAP HANA system for integration tests" 60 | ) 61 | parser.addoption( 62 | "--hana-port", type=int, 63 | help="Port of SAP HANA system" 64 | ) 65 | parser.addoption( 66 | "--hana-user", 67 | help="User for SAP HANA system" 68 | ) 69 | parser.addoption( 70 | "--hana-password", 71 | help="Password for SAP HANA user" 72 | ) 73 | parser.addoption( 74 | "--no-hana", 75 | action="store_true", 76 | help="Specify this option to omit all tests interacting with a HANA database" 77 | ) 78 | 79 | 80 | def pytest_report_header(config): 81 | hana = hana_system() 82 | if hana.host is None: 83 | return [ 84 | "WARNING: No SAP HANA host defined for integration tests" 85 | ] 86 | else: 87 | return [ 88 | "SAP HANA test system", 89 | " Host: %s:%s" % (hana.host, hana.port), 90 | " User: %s" % hana.user 91 | ] 92 | 93 | 94 | def pytest_runtest_setup(item): 95 | hana_marker = item.get_marker("hanatest") 96 | 97 | if hana_marker is not None: 98 | if item.config.getoption("--no-hana"): 99 | pytest.skip("Test requires SAP HANA system are omitted due to command line option") 100 | else: 101 | hana = hana_system() 102 | if hana.host is None: 103 | pytest.skip("Test requires SAP HANA system") 104 | -------------------------------------------------------------------------------- /tests/helper.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | import pytest 16 | 17 | 18 | def exists_table(connection, table): 19 | """Check whether table exists 20 | :param table: name of table 21 | :returns: bool 22 | """ 23 | cursor = connection.cursor() 24 | cursor.execute('SELECT 1 FROM "SYS"."TABLES" WHERE "TABLE_NAME" = %s', (table,)) 25 | return cursor.fetchone() is not None 26 | 27 | 28 | def create_table_fixture(request, connection, table, table_fields, column_table=False): 29 | """ 30 | Create table fixture for unittests 31 | :param request: pytest request object 32 | :param connection: connection object 33 | :param table: name of table 34 | :param table_fields: string with comma separated field definitions, e.g. "name VARCHAR(5), fblob blob" 35 | """ 36 | cursor = connection.cursor() 37 | if exists_table(connection, table): 38 | cursor.execute('DROP table "%s"' % table) 39 | 40 | assert not exists_table(connection, table) 41 | table_type = "COLUMN" if column_table else "" 42 | cursor.execute('CREATE %s table "%s" (%s)' % (table_type, table, table_fields)) 43 | if not exists_table(connection, table): 44 | pytest.skip("Couldn't create table %s" % table) 45 | return 46 | 47 | def _close(): 48 | cursor.execute('DROP table "%s"' % table) 49 | request.addfinalizer(_close) 50 | 51 | @pytest.fixture 52 | def procedure_add2_fixture(request, connection): 53 | cursor = connection.cursor() 54 | # create temporary procedure 55 | try: 56 | cursor.execute("""create procedure PYHDB_PROC_ADD2 (in a int, in b int, out c int, out d char) 57 | language sqlscript 58 | reads sql data as 59 | begin 60 | c := :a + :b; 61 | d := 'A'; 62 | end""") 63 | except: 64 | # procedure probably already existed 65 | pass 66 | 67 | def _close(): 68 | try: 69 | cursor.execute("""DROP PROCEDURE PYHDB_PROC_ADD2""") 70 | except: 71 | # procedure didnt exist 72 | pass 73 | 74 | request.addfinalizer(_close) 75 | 76 | @pytest.fixture 77 | def procedure_with_result_fixture(request, connection): 78 | cursor = connection.cursor() 79 | # create temporary procedure 80 | try: 81 | cursor.execute("""CREATE PROCEDURE PYHDB_PROC_WITH_RESULT (OUT OUTVAR INTEGER) 82 | AS 83 | BEGIN 84 | SELECT 2015 INTO OUTVAR FROM DUMMY; 85 | END""") 86 | except: 87 | # procedure probably already existed 88 | pass 89 | 90 | def _close(): 91 | try: 92 | cursor.execute("""DROP PROCEDURE PYHDB_PROC_WITH_RESULT""") 93 | except: 94 | # procedure didnt exist 95 | pass 96 | 97 | request.addfinalizer(_close) 98 | -------------------------------------------------------------------------------- /tests/parts/test_connectionoptions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | from io import BytesIO 16 | from pyhdb.protocol.parts import ConnectOptions 17 | from pyhdb.protocol import constants 18 | 19 | 20 | def test_pack_default_connection_options(): 21 | options = { 22 | "connection_id": None, 23 | "complete_array_execution": True, 24 | "client_locale": "en_US", 25 | "supports_large_bulk_operations": None, 26 | "large_number_of_parameters_support": None, 27 | "system_id": None, 28 | "select_for_update_supported": False, 29 | "client_distribution_mode": 0, 30 | "engine_data_format_version": None, 31 | "distribution_protocol_version": 0, 32 | "split_batch_commands": True, 33 | "use_transaction_flags_only": None, 34 | "row_and_column_optimized_format": None, 35 | "ignore_unknown_parts": None, 36 | "data_format_version": 1, 37 | "data_format_version2": 1 38 | } 39 | 40 | arguments, payload = ConnectOptions(options).pack_data(constants.MAX_SEGMENT_SIZE) 41 | assert arguments == 8 42 | # Test note: We can test again the cncatenated hex string 43 | # because sometimes the order of the dict elements is different 44 | 45 | # Contains complete_array_execution 46 | assert b"\x02\x1C\x01" in payload 47 | 48 | # Contains client_locale 49 | assert b"\x03\x1D\x05\x00\x65\x6E\x5F\x55\x53" in payload 50 | 51 | # Contains select_for_update_supported 52 | assert b"\x0E\x1C\x00" in payload 53 | 54 | # Contains client_distribution_mode 55 | assert b"\x0F\x03\x00\x00\x00\x00" in payload 56 | 57 | # Contains distribution_protocol_version 58 | assert b"\x11\x03\x00\x00\x00\x00" in payload 59 | 60 | # Contains split_batch_commands 61 | assert b"\x12\x1C\x01" in payload 62 | 63 | # Contains data_format_version 64 | assert b"\x0C\x03\x01\x00\x00\x00" in payload 65 | 66 | # Contains data_format_version2 67 | assert b"\x17\x03\x01\x00\x00\x00" in payload 68 | 69 | # There is nothing more 70 | assert len(payload) == 42 71 | 72 | 73 | def test_unpack_default_connection_options(): 74 | packed = BytesIO( 75 | b"\x03\x1d\x05\x00\x65\x6e\x5f\x55\x53\x0f\x03\x00\x00\x00\x00\x17" 76 | b"\x03\x01\x00\x00\x00\x0c\x03\x01\x00\x00\x00\x02\x1c\x01\x11\x03" 77 | b"\x00\x00\x00\x00\x0e\x1c\x00\x12\x1c\x01" 78 | ) 79 | 80 | options, = ConnectOptions.unpack_data(8, packed) 81 | assert options == { 82 | "complete_array_execution": True, 83 | "client_locale": "en_US", 84 | "select_for_update_supported": False, 85 | "client_distribution_mode": 0, 86 | "distribution_protocol_version": 0, 87 | "split_batch_commands": True, 88 | "data_format_version": 1, 89 | "data_format_version2": 1 90 | } 91 | -------------------------------------------------------------------------------- /pyhdb/auth.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | import struct 17 | import hashlib 18 | import hmac 19 | from io import BytesIO 20 | ### 21 | from pyhdb.protocol.segments import RequestSegment 22 | from pyhdb.protocol.constants import message_types 23 | from pyhdb.protocol.parts import Authentication, Fields 24 | from pyhdb.protocol.message import RequestMessage 25 | from pyhdb.compat import iter_range 26 | 27 | CLIENT_PROOF_SIZE = 32 28 | CLIENT_KEY_SIZE = 64 29 | 30 | 31 | class AuthManager(object): 32 | 33 | def __init__(self, connection, user, password): 34 | self.connection = connection 35 | self.user = user 36 | self.password = password 37 | 38 | self.method = b"SCRAMSHA256" 39 | self.client_key = os.urandom(CLIENT_KEY_SIZE) 40 | self.client_proof = None 41 | 42 | def perform_handshake(self): 43 | request = RequestMessage.new( 44 | self.connection, 45 | RequestSegment( 46 | message_types.AUTHENTICATE, 47 | Authentication(self.user, {self.method: self.client_key}) 48 | ) 49 | ) 50 | response = self.connection.send_request(request) 51 | 52 | auth_part = response.segments[0].parts[0] 53 | if self.method not in auth_part.methods: 54 | raise Exception( 55 | "Only unknown authentication methods available: %s" % 56 | b",".join(auth_part.methods.keys()) 57 | ) 58 | 59 | salt, server_key = Fields.unpack_data( 60 | BytesIO(auth_part.methods[self.method]) 61 | ) 62 | 63 | self.client_proof = self.calculate_client_proof([salt], server_key) 64 | return Authentication(self.user, {'SCRAMSHA256': self.client_proof}) 65 | 66 | def calculate_client_proof(self, salts, server_key): 67 | proof = b"\x00" 68 | proof += struct.pack('b', len(salts)) 69 | 70 | for salt in salts: 71 | proof += struct.pack('b', CLIENT_PROOF_SIZE) 72 | proof += self.scramble_salt(salt, server_key) 73 | 74 | return proof 75 | 76 | def scramble_salt(self, salt, server_key): 77 | msg = salt + server_key + self.client_key 78 | 79 | key = hashlib.sha256( 80 | hmac.new( 81 | self.password.encode('cesu-8'), salt, hashlib.sha256 82 | ).digest() 83 | ).digest() 84 | key_hash = hashlib.sha256(key).digest() 85 | 86 | sig = hmac.new( 87 | key_hash, msg, hashlib.sha256 88 | ).digest() 89 | 90 | return self._xor(sig, key) 91 | 92 | @staticmethod 93 | def _xor(a, b): 94 | a = bytearray(a) 95 | b = bytearray(b) 96 | result = bytearray(len(a)) 97 | for i in iter_range(len(a)): 98 | result[i] += a[i] ^ b[i] 99 | return bytes(result) 100 | -------------------------------------------------------------------------------- /pyhdb/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | 17 | from pyhdb.exceptions import * 18 | from pyhdb.connection import Connection 19 | from pyhdb.protocol.lobs import Blob, Clob, NClob 20 | from pyhdb.compat import configparser 21 | 22 | apilevel = "2.0" 23 | threadsafety = 2 24 | paramstyle = "numeric" 25 | tracing = os.environ.get('HDB_TRACE', 'FALSE').upper() in ('TRUE', '1') 26 | 27 | 28 | def connect(host, port, user, password, autocommit=False): 29 | conn = Connection(host, port, user, password, autocommit) 30 | conn.connect() 31 | return conn 32 | 33 | 34 | def from_ini(ini_file, section=None): 35 | """ 36 | Make connection to database by reading connection parameters from an ini file. 37 | :param ini_file: Name of ini file, e.g. 'pytest.ini' 38 | :param section: specify alternative section in ini file. Section 'hana' and 'pytest' will be searched by default 39 | :return: connection object 40 | 41 | Example: 42 | [pytest] 43 | hana_host = 10.97.76.24 44 | hana_hostname = mo-2384d0f48.mo.sap.corp 45 | hana_port = 30015 46 | hana_user = D037732 47 | hana_password = Abcd1234 48 | 49 | For historical reasons a 'hana_' prefix is allowed, but will be removed automatically. 50 | """ 51 | if not os.path.exists(ini_file): 52 | raise RuntimeError('Could not find ini file %s' % ini_file) 53 | cp = configparser.ConfigParser() 54 | cp.read(ini_file) 55 | if not cp.sections(): 56 | raise RuntimeError('Could not find any section in ini file %s' % ini_file) 57 | if section: 58 | sec_list = [section] 59 | elif len(cp.sections()) == 1: 60 | # no section specified - check if there is a single/unique section in the ini file: 61 | sec_list = cp.sections() 62 | else: 63 | # ini_file has more than one section, so try some default names: 64 | sec_list = ['hana', 'pytest'] 65 | 66 | for sec in sec_list: 67 | try: 68 | param_values = cp.items(sec) 69 | except configparser.NoSectionError: 70 | continue 71 | params = dict(param_values) 72 | break 73 | else: 74 | raise RuntimeError('Could not guess which section to use for hana credentials from %s' % ini_file) 75 | 76 | # Parameters can be named like 'hana_user' (e.g. pytest.ini) or just 'user' (other ini's). 77 | # Remove the 'hana_' prefix so that parameter names match the arguments of the pyhdb.connect() function. 78 | # Also remove invalid keys from clean_params (like 'hostname' etc). 79 | 80 | def rm_prefix(param): 81 | return param[5:] if param.startswith('hana_') else param 82 | 83 | valid_keys = ('host', 'port', 'user', 'password') 84 | clean_params = {'%s' % rm_prefix(key): val for key, val in params.items() if rm_prefix(key) in valid_keys} 85 | 86 | # make actual connection: 87 | return connect(**clean_params) 88 | 89 | 90 | # Add from_ini() as attribute to the connect method, so to use it do: pyhdb.connect.from_ini(ini_file) 91 | connect.from_ini = from_ini 92 | # ... and cleanup the local namespace: 93 | del from_ini 94 | -------------------------------------------------------------------------------- /tests/test_cesu8.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2014, 2015 SAP SE. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http: //www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 14 | # either express or implied. See the License for the specific 15 | # language governing permissions and limitations under the License. 16 | 17 | import pytest 18 | import pyhdb.cesu8 # import required to register cesu8 encoding 19 | 20 | 21 | @pytest.mark.parametrize("encoded,unicode_obj", [ 22 | (b"\xed\xa6\x9d\xed\xbd\xb7", u"\U00077777"), 23 | (b"\xed\xa0\x80\xed\xb0\xb0", u"\U00010030"), 24 | (b"\xed\xa0\x80\xed\xbc\xb0", u"\U00010330"), 25 | (b"\xed\xa0\x81\xed\xb0\x80", u"\U00010400"), 26 | (b"\xed\xa0\xbd\xed\xb1\x86", u"\U0001F446"), 27 | (b"\xed\xa0\xbd\xed\xb2\x86", u"\U0001f486"), 28 | (b"\xed\xa0\xbd\xed\xb8\x86", u"\U0001f606"), 29 | (b"\xed\xa0\xbf\xed\xbc\x84", u"\U0001FF04"), 30 | ]) 31 | def test_pure_cesu8_decode(encoded, unicode_obj): 32 | assert encoded.decode('cesu-8') == unicode_obj 33 | 34 | 35 | @pytest.mark.parametrize("encoded,unicode_obj", [ 36 | (b"\xc3\xa4", u"\xe4"), 37 | (b"\xe2\xac\xa1", u"\u2b21"), 38 | ]) 39 | def test_fallback_to_utf8_of_cesu8_decode(encoded, unicode_obj): 40 | assert encoded.decode('cesu-8') == unicode_obj 41 | 42 | 43 | def test_multiple_chars_in_cesu8_decode(): 44 | encoded = b"\xed\xa0\xbd\xed\xb0\x8d\xed\xa0\xbd\xed\xb1\x8d" 45 | assert encoded.decode('cesu-8') == u'\U0001f40d\U0001f44d' 46 | 47 | 48 | def test_cesu8_and_utf8_mixed_decode(): 49 | encoded = b"\xed\xa0\xbd\xed\xb0\x8d\x20\x69\x73\x20\x61\x20" \ 50 | b"\xcf\x86\xce\xaf\xce\xb4\xce\xb9" 51 | assert encoded.decode('cesu-8') == \ 52 | u'\U0001f40d is a \u03c6\u03af\u03b4\u03b9' 53 | 54 | 55 | @pytest.mark.parametrize("encoded,unicode_obj", [ 56 | (b"\xed\xa6\x9d\xed\xbd\xb7", u"\U00077777"), 57 | (b"\xed\xa0\x80\xed\xb0\xb0", u"\U00010030"), 58 | (b"\xed\xa0\x80\xed\xbc\xb0", u"\U00010330"), 59 | (b"\xed\xa0\x81\xed\xb0\x80", u"\U00010400"), 60 | (b"\xed\xa0\xbd\xed\xb1\x86", u"\U0001F446"), 61 | (b"\xed\xa0\xbd\xed\xb2\x86", u"\U0001f486"), 62 | (b"\xed\xa0\xbd\xed\xb8\x86", u"\U0001f606"), 63 | (b"\xed\xa0\xbf\xed\xbc\x84", u"\U0001FF04"), 64 | ]) 65 | def test_pure_cesu8_encode(encoded, unicode_obj): 66 | assert unicode_obj.encode('cesu-8') == encoded 67 | 68 | 69 | @pytest.mark.parametrize("encoded,unicode_obj", [ 70 | (b"\xc3\xa4", u"\xe4"), 71 | (b"\xe2\xac\xa1", u"\u2b21"), 72 | ]) 73 | def test_fallback_to_utf8_encode(encoded, unicode_obj): 74 | assert unicode_obj.encode('cesu-8') == encoded 75 | 76 | 77 | def test_multiple_chars_in_cesu8_encode(): 78 | encoded = b"\xed\xa0\xbd\xed\xb0\x8d\xed\xa0\xbd\xed\xb1\x8d" 79 | assert u'\U0001f40d\U0001f44d'.encode('cesu-8') == encoded 80 | 81 | 82 | def test_cesu8_and_utf8_mixed_encode(): 83 | encoded = b"\xed\xa0\xbd\xed\xb0\x8d\x20\x69\x73\x20\x61\x20" \ 84 | b"\xcf\x86\xce\xaf\xce\xb4\xce\xb9" 85 | assert u'\U0001f40d is a \u03c6\u03af\u03b4\u03b9'.encode('cesu-8') == \ 86 | encoded 87 | 88 | def test_cesu8_and_utf8_mixed_encode_decode(): 89 | unicode_input = u'Some normal text with 😊 and other emojis like 🤔 or 🤖' 90 | cesu8_encoded = unicode_input.encode('cesu-8') 91 | decoded_unicode = cesu8_encoded.decode('cesu-8') 92 | assert decoded_unicode == unicode_input 93 | -------------------------------------------------------------------------------- /tests/test_transactions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | import pytest 16 | 17 | import pyhdb 18 | import tests.helper 19 | 20 | TABLE = 'PYHDB_TEST_1' 21 | TABLE_FIELDS = 'TEST VARCHAR(255)' 22 | 23 | 24 | @pytest.fixture 25 | def test_table(request, connection): 26 | """Fixture to create table for testing, and dropping it after test run""" 27 | tests.helper.create_table_fixture(request, connection, TABLE, TABLE_FIELDS) 28 | 29 | 30 | @pytest.mark.hanatest 31 | class TestIsolationBetweenConnections(object): 32 | 33 | def test_commit(self, request, hana_system, test_table): 34 | connection_1 = pyhdb.connect(*hana_system) 35 | connection_2 = pyhdb.connect(*hana_system) 36 | 37 | def _close(): 38 | connection_1.close() 39 | connection_2.close() 40 | request.addfinalizer(_close) 41 | 42 | cursor1 = connection_1.cursor() 43 | cursor1.execute( 44 | 'INSERT INTO PYHDB_TEST_1 VALUES(%s)', ('connection_1',) 45 | ) 46 | cursor1.execute("SELECT * FROM PYHDB_TEST_1") 47 | assert cursor1.fetchall() == [('connection_1',)] 48 | 49 | cursor2 = connection_2.cursor() 50 | cursor2.execute("SELECT * FROM PYHDB_TEST_1") 51 | assert cursor2.fetchall() == [] 52 | 53 | connection_1.commit() 54 | 55 | cursor2.execute("SELECT * FROM PYHDB_TEST_1") 56 | assert cursor2.fetchall() == [('connection_1',)] 57 | 58 | def test_rollback(self, request, hana_system, test_table): 59 | connection_1 = pyhdb.connect(*hana_system) 60 | connection_2 = pyhdb.connect(*hana_system) 61 | 62 | def _close(): 63 | connection_1.close() 64 | connection_2.close() 65 | request.addfinalizer(_close) 66 | 67 | cursor1 = connection_1.cursor() 68 | cursor1.execute( 69 | 'INSERT INTO PYHDB_TEST_1 VALUES(%s)', ('connection_1',) 70 | ) 71 | cursor1.execute("SELECT * FROM PYHDB_TEST_1") 72 | assert cursor1.fetchall() == [('connection_1',)] 73 | 74 | cursor2 = connection_2.cursor() 75 | cursor2.execute("SELECT * FROM PYHDB_TEST_1") 76 | assert cursor2.fetchall() == [] 77 | 78 | connection_1.rollback() 79 | 80 | cursor1.execute("SELECT * FROM PYHDB_TEST_1") 81 | assert cursor1.fetchall() == [] 82 | 83 | def test_auto_commit(self, request, hana_system, test_table): 84 | connection_1 = pyhdb.connect(*hana_system, autocommit=True) 85 | connection_2 = pyhdb.connect(*hana_system, autocommit=True) 86 | 87 | def _close(): 88 | connection_1.close() 89 | connection_2.close() 90 | request.addfinalizer(_close) 91 | 92 | cursor1 = connection_1.cursor() 93 | cursor1.execute( 94 | 'INSERT INTO PYHDB_TEST_1 VALUES(%s)', ('connection_1',) 95 | ) 96 | cursor1.execute("SELECT * FROM PYHDB_TEST_1") 97 | assert cursor1.fetchall() == [('connection_1',)] 98 | 99 | cursor2 = connection_2.cursor() 100 | cursor2.execute("SELECT * FROM PYHDB_TEST_1") 101 | assert cursor2.fetchall() == [('connection_1',)] 102 | 103 | def test_select_for_update(self, connection, test_table): 104 | cursor = connection.cursor() 105 | cursor.execute("INSERT INTO PYHDB_TEST_1 VALUES(%s)", ('test',)) 106 | connection.commit() 107 | 108 | cursor.execute("SELECT * FROM PYHDB_TEST_1 FOR UPDATE") 109 | assert cursor.fetchall() == [('test',)] 110 | 111 | -------------------------------------------------------------------------------- /pyhdb/lib/tracing.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from __future__ import print_function 15 | import io 16 | import pyhdb 17 | 18 | 19 | def trace(trace_obj): 20 | """Print recursive trace of given network protocol object 21 | :param trace_obj: either a message, segement, or kind object 22 | """ 23 | if pyhdb.tracing: 24 | t = TraceLogger() 25 | tr = t.trace(trace_obj) 26 | print(tr) 27 | return tr 28 | 29 | 30 | class TraceLogger(object): 31 | """Trace logger class for dumping header and binary data of messages, segments, and parts""" 32 | _indent_incr = 4 33 | 34 | def __init__(self): 35 | self._indent_level = 0 36 | self._indent_level_is_first = {0: True} 37 | self._buffer = io.StringIO() 38 | 39 | def trace(self, trace_obj): 40 | """ 41 | Trace given trace_obj (usuall a Message instance) recursively. 42 | :param trace_obj: 43 | :return: a string with properly formatted tracing information 44 | """ 45 | tracer = self 46 | tracer.writeln(u'%s = ' % trace_obj.__class__.__name__) 47 | tracer.incr('{') 48 | for attr_name in trace_obj.__tracing_attrs__: 49 | attr = getattr(trace_obj, attr_name) 50 | if isinstance(attr, tuple) and hasattr(attr, '_fields'): 51 | # probably a namedtuple instance 52 | tracer.writeln(u'%s = ' % (attr_name,)) 53 | tracer.incr('[') 54 | for k, v in attr._asdict().items(): 55 | # _asdict() creates an OrderedDict, so elements are still in order 56 | tracer.writeln(u'%s = %s' % (k, v)) 57 | tracer.decr(']') 58 | elif isinstance(attr, (list, tuple)): 59 | if attr: 60 | tracer.writeln(u'%s = ' % (attr_name,)) 61 | tracer.incr('[') 62 | for elem in attr: 63 | if hasattr(elem, '__tracing_attrs__'): 64 | self.trace(elem) 65 | else: 66 | # some other plain list element, just print it as it is: 67 | tracer.writeln(u'%s' % repr(elem)) 68 | tracer.decr(']') 69 | else: 70 | tracer.writeln(u'%s = []' % (attr_name,)) 71 | else: 72 | # a plain attribute object, just print it as it is 73 | tracer.writeln(u'%s = %s' % (attr_name, repr(attr))) 74 | tracer.decr('}') 75 | return self.getvalue() 76 | 77 | def incr(self, brace): 78 | self._buffer.write(u'%s\n' % brace) 79 | self._indent_level += self._indent_incr 80 | self._indent_level_is_first[self._indent_level] = True 81 | 82 | def decr(self, brace): 83 | assert self._indent_level > 0, 'Indentation level cannot be decremented any further' 84 | self._buffer.write(u'\n') 85 | self._indent_level -= self._indent_incr 86 | self._buffer.write(u' ' * self._indent_level) 87 | self._buffer.write(u'%s' % brace) 88 | 89 | def writeln(self, line): 90 | if self._indent_level_is_first[self._indent_level]: 91 | self._indent_level_is_first[self._indent_level] = False 92 | else: 93 | self._buffer.write(u',\n') 94 | 95 | self._buffer.write(u' ' * self._indent_level) 96 | self._buffer.write(line) 97 | 98 | def getvalue(self): 99 | return self._buffer.getvalue() 100 | -------------------------------------------------------------------------------- /tests/test_dummy_sql.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | import random 16 | import datetime 17 | from decimal import Decimal, getcontext 18 | 19 | import pytest 20 | 21 | 22 | @pytest.mark.hanatest 23 | def test_dummy_sql_int(connection): 24 | cursor = connection.cursor() 25 | cursor.execute("SELECT 1 FROM DUMMY") 26 | 27 | result = cursor.fetchone() 28 | assert result == (1,) 29 | 30 | 31 | @pytest.mark.hanatest 32 | def test_dummy_sql_decimal(connection): 33 | getcontext().prec = 36 34 | 35 | cursor = connection.cursor() 36 | cursor.execute("SELECT -312313212312321.1245678910111213142 FROM DUMMY") 37 | 38 | result = cursor.fetchone() 39 | assert result == (Decimal('-312313212312321.1245678910111213142'),) 40 | 41 | 42 | @pytest.mark.hanatest 43 | def test_dummy_sql_string(connection): 44 | cursor = connection.cursor() 45 | cursor.execute("SELECT 'Hello World' FROM DUMMY") 46 | 47 | result = cursor.fetchone() 48 | assert result == ("Hello World",) 49 | 50 | 51 | @pytest.mark.hanatest 52 | def test_dummy_sql_long_string(connection): 53 | test_string = '%030x' % random.randrange(16**300) 54 | 55 | cursor = connection.cursor() 56 | cursor.execute("SELECT '%s' FROM DUMMY" % test_string) 57 | 58 | result = cursor.fetchone() 59 | assert result == (test_string,) 60 | 61 | 62 | @pytest.mark.hanatest 63 | def test_dummy_sql_binary(connection): 64 | cursor = connection.cursor() 65 | cursor.execute("SELECT X'FF00FFA3B5' FROM DUMMY") 66 | 67 | result = cursor.fetchone() 68 | assert result == (b"\xFF\x00\xFF\xA3\xB5",) 69 | 70 | 71 | @pytest.mark.hanatest 72 | def test_dummy_sql_current_time(connection): 73 | cursor = connection.cursor() 74 | cursor.execute("SELECT current_time FROM DUMMY") 75 | 76 | result = cursor.fetchone() 77 | assert isinstance(result[0], datetime.time) 78 | 79 | 80 | @pytest.mark.hanatest 81 | def test_dummy_sql_to_time(connection): 82 | now = datetime.datetime.now().time() 83 | 84 | cursor = connection.cursor() 85 | cursor.execute("SELECT to_time(%s) FROM DUMMY", (now,)) 86 | 87 | result = cursor.fetchone() 88 | 89 | # No support of microsecond 90 | assert result[0] == now.replace(microsecond=0) 91 | 92 | 93 | @pytest.mark.hanatest 94 | def test_dummy_sql_current_date(connection): 95 | cursor = connection.cursor() 96 | cursor.execute("SELECT current_date FROM DUMMY") 97 | 98 | result = cursor.fetchone() 99 | assert isinstance(result[0], datetime.date) 100 | 101 | 102 | @pytest.mark.hanatest 103 | def test_dummy_sql_to_date(connection): 104 | today = datetime.date.today() 105 | 106 | cursor = connection.cursor() 107 | cursor.execute("SELECT to_date(%s) FROM DUMMY", (today,)) 108 | 109 | result = cursor.fetchone() 110 | assert result[0] == today 111 | 112 | 113 | @pytest.mark.hanatest 114 | def test_dummy_sql_current_timestamp(connection): 115 | cursor = connection.cursor() 116 | cursor.execute("SELECT current_timestamp FROM DUMMY") 117 | 118 | result = cursor.fetchone() 119 | assert isinstance(result[0], datetime.datetime) 120 | 121 | 122 | @pytest.mark.hanatest 123 | def test_dummy_sql_to_timestamp(connection): 124 | now = datetime.datetime.now() 125 | now = now.replace(microsecond=123000) 126 | 127 | cursor = connection.cursor() 128 | cursor.execute("SELECT to_timestamp(%s) FROM DUMMY", (now,)) 129 | 130 | result = cursor.fetchone() 131 | assert result[0] == now 132 | 133 | 134 | @pytest.mark.hanatest 135 | def test_dummy_sql_without_result(connection): 136 | cursor = connection.cursor() 137 | cursor.execute("SELECT 1 FROM DUMMY WHERE 1 != 1") 138 | 139 | result = cursor.fetchone() 140 | assert result is None 141 | -------------------------------------------------------------------------------- /tests/types/test_datetime.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | from io import BytesIO 16 | from datetime import datetime, time, date 17 | 18 | import pytest 19 | from pyhdb.protocol import types 20 | 21 | 22 | # ########################## Test value unpacking ##################################### 23 | 24 | @pytest.mark.parametrize("input,expected", [ 25 | (b'\x90\x0C\xD8\x59', time(16, 12, 23)), 26 | (b"\x00\x00\x00\x00", None), 27 | ]) 28 | def test_unpack_time(input, expected): 29 | input = BytesIO(input) 30 | assert types.Time.from_resultset(input) == expected 31 | 32 | 33 | @pytest.mark.parametrize("input,expected", [ 34 | (time(8, 9, 50), "'08:09:50'"), 35 | (time(23, 59, 59), "'23:59:59'"), 36 | ]) 37 | def test_escape_date(input, expected): 38 | assert types.Time.to_sql(input) == expected 39 | 40 | 41 | @pytest.mark.parametrize("input,expected", [ 42 | (b'\xDE\x87\x07\x16', date(2014, 8, 22)), 43 | (b"\x00\x00\x00\x00", None), 44 | ]) 45 | def test_unpack_date(input, expected): 46 | input = BytesIO(input) 47 | assert types.Date.from_resultset(input) == expected 48 | 49 | 50 | @pytest.mark.parametrize("input,expected", [ 51 | (date(2014, 8, 22), "'2014-08-22'"), 52 | (date(1988, 3, 23), "'1988-03-23'"), 53 | ]) 54 | def test_escape_date(input, expected): 55 | assert types.Date.to_sql(input) == expected 56 | 57 | 58 | @pytest.mark.parametrize("input,expected", [ 59 | (b'\xDE\x87\x07\x19\x89\x2F\xC8\x01', 60 | datetime(2014, 8, 25, 9, 47, 0, 456000)), 61 | (b"\x00\x00\x00\x00\x00\x00\x00\x00", None), 62 | ]) 63 | def test_unpack_timestamp(input, expected): 64 | input = BytesIO(input) 65 | assert types.Timestamp.from_resultset(input) == expected 66 | 67 | 68 | @pytest.mark.parametrize("input,expected", [ 69 | (datetime(2014, 8, 22, 8, 9, 50), "'2014-08-22 08:09:50.0'"), 70 | (datetime(1988, 3, 23, 23, 59, 50, 123), "'1988-03-23 23:59:50.123'"), 71 | ]) 72 | def test_escape_timestamp(input, expected): 73 | assert types.Timestamp.to_sql(input) == expected 74 | 75 | 76 | @pytest.mark.parametrize("input,expected", [ 77 | (date(2014, 2, 18), 735284), 78 | (date(1582, 10, 15), 577738), 79 | (date(1582, 10, 4), 577737), 80 | (date(1, 1, 1), 1), 81 | ]) 82 | def test_to_daydate(input, expected): 83 | assert types.Date.to_daydate(input) == expected 84 | 85 | 86 | # ########################## Test value packing ##################################### 87 | 88 | @pytest.mark.parametrize("input,expected", [ 89 | (datetime(2014, 8, 25, 9, 47, 3), b'\x10\xDE\x87\x07\x19\x89\x2F\xb8\x0b'), 90 | (datetime(2014, 8, 25, 9, 47, 3, 2000), b'\x10\xDE\x87\x07\x19\x89\x2F\xba\x0b'), 91 | (datetime(2014, 8, 25, 9, 47, 3, 999000), b'\x10\xDE\x87\x07\x19\x89\x2F\x9f\x0f'), 92 | (datetime(2014, 8, 25, 9, 47, 3, 999999), b'\x10\xDE\x87\x07\x19\x89\x2F\x9f\x0f'), 93 | ("2014-08-25 09:47:03", b'\x10\xDE\x87\x07\x19\x89\x2F\xb8\x0b'), 94 | ("2014-08-25 09:47:03.002000", b'\x10\xDE\x87\x07\x19\x89\x2F\xba\x0b'), 95 | ]) 96 | def test_pack_timestamp(input, expected): 97 | assert types.Timestamp.prepare(input) == expected 98 | 99 | 100 | @pytest.mark.parametrize("input,expected", [ 101 | (date(2014, 8, 25), b'\x0e\xDE\x87\x07\x19'), 102 | ("2014-8-25", b'\x0e\xDE\x87\x07\x19'), 103 | ]) 104 | def test_pack_date(input, expected): 105 | assert types.Date.prepare(input) == expected 106 | 107 | 108 | @pytest.mark.parametrize("input,expected", [ 109 | (time(9, 47, 3), b'\x0f\x89/\xb8\x0b'), 110 | (time(9, 47, 3, 2000), b'\x0f\x89\x2F\xba\x0b'), 111 | (time(9, 47, 3, 999000), b'\x0f\x89\x2F\x9f\x0f'), 112 | (time(9, 47, 3, 999999), b'\x0f\x89\x2F\x9f\x0f'), 113 | ("9:47:03", b'\x0f\x89/\xb8\x0b'), 114 | ("9:47:03.002000", b'\x0f\x89\x2F\xba\x0b'), 115 | ]) 116 | def test_pack_time(input, expected): 117 | assert types.Time.prepare(input) == expected 118 | 119 | -------------------------------------------------------------------------------- /pyhdb/protocol/message.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import io 16 | import struct 17 | ### 18 | from pyhdb.protocol import constants 19 | from pyhdb.protocol.headers import MessageHeader 20 | from pyhdb.protocol.segments import ReplySegment 21 | from pyhdb.lib.tracing import trace 22 | 23 | 24 | class BaseMessage(object): 25 | """ 26 | Message - basic frame for sending to and receiving data from HANA db. 27 | """ 28 | header_struct = struct.Struct('qiIIhb9x') # I8 I4 UI4 UI4 I2 I1 x[9] 29 | header_size = header_struct.size 30 | assert header_size == constants.general.MESSAGE_HEADER_SIZE # Ensures that the constant defined there is correct! 31 | __tracing_attrs__ = ['header', 'segments'] 32 | 33 | def __init__(self, session_id, packet_count, segments=(), autocommit=False, header=None): 34 | self.session_id = session_id 35 | self.packet_count = packet_count 36 | self.autocommit = autocommit 37 | self.segments = segments if isinstance(segments, (list, tuple)) else (segments, ) 38 | self.header = header 39 | 40 | 41 | class RequestMessage(BaseMessage): 42 | def build_payload(self, payload): 43 | """ Build payload of message. """ 44 | for segment in self.segments: 45 | segment.pack(payload, commit=self.autocommit) 46 | 47 | def pack(self): 48 | """ Pack message to binary stream. """ 49 | payload = io.BytesIO() 50 | # Advance num bytes equal to header size - the header is written later 51 | # after the payload of all segments and parts has been written: 52 | payload.seek(self.header_size, io.SEEK_CUR) 53 | 54 | # Write out payload of segments and parts: 55 | self.build_payload(payload) 56 | 57 | packet_length = len(payload.getvalue()) - self.header_size 58 | self.header = MessageHeader(self.session_id, self.packet_count, packet_length, constants.MAX_SEGMENT_SIZE, 59 | num_segments=len(self.segments), packet_options=0) 60 | packed_header = self.header_struct.pack(*self.header) 61 | 62 | # Go back to begining of payload for writing message header: 63 | payload.seek(0) 64 | payload.write(packed_header) 65 | payload.seek(0, io.SEEK_END) 66 | 67 | trace(self) 68 | 69 | return payload 70 | 71 | @classmethod 72 | def new(cls, connection, segments=()): 73 | """Return a new request message instance - extracts required data from connection object 74 | :param connection: connection object 75 | :param segments: a single segment instance, or a list/tuple of segment instances 76 | :returns: RequestMessage instance 77 | """ 78 | return cls(connection.session_id, connection.get_next_packet_count(), segments, 79 | autocommit=connection.autocommit) 80 | 81 | 82 | class ReplyMessage(BaseMessage): 83 | """Reply message class""" 84 | @classmethod 85 | def unpack_reply(cls, header, payload): 86 | """Take already unpacked header and binary payload of received request reply and creates message instance 87 | :param header: a namedtuple header object providing header information 88 | :param payload: payload (BytesIO instance) of message 89 | """ 90 | reply = cls( 91 | header.session_id, header.packet_count, 92 | segments=tuple(ReplySegment.unpack_from(payload, expected_segments=header.num_segments)), 93 | header=header 94 | ) 95 | trace(reply) 96 | return reply 97 | 98 | @classmethod 99 | def header_from_raw_header_data(cls, raw_header): 100 | """Unpack binary message header data obtained as a reply from HANA 101 | :param raw_header: binary string containing message header data 102 | :returns: named tuple for easy access of header data 103 | """ 104 | try: 105 | header = MessageHeader(*cls.header_struct.unpack(raw_header)) 106 | except struct.error: 107 | raise Exception("Invalid message header received") 108 | return header 109 | -------------------------------------------------------------------------------- /tests/types/test_string.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | import os 16 | import random 17 | from io import BytesIO 18 | import string 19 | ### 20 | import pytest 21 | ### 22 | from pyhdb.protocol import types 23 | from pyhdb.exceptions import InterfaceError 24 | from pyhdb.compat import byte_type, iter_range 25 | 26 | 27 | # ########################## Test value unpacking ##################################### 28 | 29 | @pytest.mark.parametrize("given,expected", [ 30 | (b"\xFF", None), 31 | (b"\x0B\x48\x65\x6C\x6C\x6F\x20\x57\x6F\x72\x6C\x64", "Hello World"), 32 | ]) 33 | def test_unpack_string(given, expected): 34 | given = BytesIO(given) 35 | assert types.String.from_resultset(given) == expected 36 | 37 | 38 | def test_unpack_long_string(): 39 | text = u'%030x' % random.randrange(16**300) 40 | given = BytesIO(b"\xF6\x2C\x01" + text.encode('cesu-8')) 41 | assert types.String.from_resultset(given) == text 42 | 43 | 44 | def test_unpack_very_long_string(): 45 | text = u'%030x' % random.randrange(16**30000) 46 | given = BytesIO(b"\xF7\x30\x75\x00\x00" + text.encode('cesu-8')) 47 | assert types.String.from_resultset(given) == text 48 | 49 | 50 | def test_unpack_invalid_string_length_indicator(): 51 | # The string length indicator 254 is not definied 52 | with pytest.raises(InterfaceError): 53 | types.String.from_resultset(BytesIO(b"\xFE")) 54 | 55 | 56 | @pytest.mark.parametrize("given,expected", [ 57 | (b"\xFF", None), 58 | ( 59 | b"\x0B\xFF\x00\xFF\x00\xFF\x00\xFF\x00\xFF\x00\xFF", 60 | byte_type(b"\xFF\x00\xFF\x00\xFF\x00\xFF\x00\xFF\x00\xFF") 61 | ), 62 | ]) 63 | def test_unpack_binary(given, expected): 64 | given = BytesIO(given) 65 | assert types.Binary.from_resultset(given) == expected 66 | 67 | 68 | def test_unpack_long_binary(): 69 | binary_data = os.urandom(300) 70 | given = BytesIO(b"\xF6\x2C\x01" + binary_data) 71 | assert types.Binary.from_resultset(given) == binary_data 72 | 73 | 74 | def test_unpack_very_long_binary(): 75 | binary_data = os.urandom(30000) 76 | given = BytesIO(b"\xF7\x30\x75\x00\x00" + binary_data) 77 | assert types.Binary.from_resultset(given) == binary_data 78 | 79 | 80 | @pytest.mark.parametrize("given,expected", [ 81 | (byte_type(b"\xFF\x00\xFF\xA3\x5B"), "'ff00ffa35b'"), 82 | (byte_type(b"\x75\x08\x15\xBB\xAA"), "'750815bbaa'"), 83 | ]) 84 | def test_escape_binary(given, expected): 85 | assert types.Binary.to_sql(given) == expected 86 | 87 | 88 | # ########################## Test value packing ##################################### 89 | 90 | @pytest.mark.parametrize("given,expected", [ 91 | (None, b"\x08\xFF", ), 92 | ('123', b"\x08\x03123"), # convert an integer into its string representation 93 | ("Hello World", b"\x08\x0B\x48\x65\x6C\x6C\x6F\x20\x57\x6F\x72\x6C\x64"), 94 | ]) 95 | def test_pack_string(given, expected): 96 | assert types.String.prepare(given) == expected 97 | 98 | 99 | def test_pack_long_string(): 100 | text = b'\xe6\x9c\xb1' * 3500 101 | expected = b"\x08\xF6\x04\x29" + text 102 | assert types.String.prepare(text.decode('cesu-8')) == expected 103 | 104 | 105 | def test_pack_very_long_string(): 106 | text = b'\xe6\x9c\xb1' * 35000 107 | expected = b"\x08\xF7\x28\x9a\x01\x00" + text 108 | assert types.String.prepare(text.decode('cesu-8')) == expected 109 | 110 | 111 | # ############################################################################################################# 112 | # Real HANA interaction with strings (integration tests) 113 | # ############################################################################################################# 114 | 115 | import tests.helper 116 | TABLE = 'PYHDB_TEST_STRING' 117 | TABLE_FIELDS = 'name varchar(5000)' # 5000 chars is maximum for VARCHAR field 118 | 119 | 120 | @pytest.fixture 121 | def test_table(request, connection): 122 | """Fixture to create table for testing lobs, and dropping it after test run""" 123 | tests.helper.create_table_fixture(request, connection, TABLE, TABLE_FIELDS) 124 | 125 | 126 | @pytest.mark.hanatest 127 | def test_insert_string(connection, test_table): 128 | """Insert string into table""" 129 | cursor = connection.cursor() 130 | large_string = ''.join(random.choice(string.ascii_letters) for _ in iter_range(5000)) 131 | cursor.execute("insert into %s (name) values (:1)" % TABLE, [large_string]) 132 | connection.commit() 133 | cursor = connection.cursor() 134 | row = cursor.execute('select name from %s' % TABLE).fetchone() 135 | assert row[0] == large_string 136 | -------------------------------------------------------------------------------- /pyhdb/protocol/headers.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | import collections 16 | import struct 17 | ### 18 | from pyhdb.protocol.constants import type_codes 19 | 20 | 21 | MessageHeader = collections.namedtuple( 22 | 'MessageHeader', 'session_id, packet_count, payload_length, varpartsize, num_segments, packet_options') 23 | 24 | 25 | RequestSegmentHeader = collections.namedtuple( 26 | 'RequestSegmentHeader', 27 | 'segment_length, segment_offset, num_parts, segment_number, segment_kind, message_type, commit, command_options') 28 | 29 | 30 | ReplySegmentHeader = collections.namedtuple( 31 | 'ReplySegmentHeader', 32 | 'segment_length, segment_offset, num_parts, segment_number, segment_kind, function_code') 33 | 34 | 35 | PartHeader = collections.namedtuple( 36 | 'PartHeader', 37 | 'part_kind, part_attributes, argument_count, bigargument_count, payload_size, remaining_buffer_size') 38 | 39 | 40 | class BaseLobheader(object): 41 | """Base LobHeader class""" 42 | BLOB_TYPE = 1 43 | CLOB_TYPE = 2 44 | NCLOB_TYPE = 3 45 | 46 | LOB_TYPES = {type_codes.BLOB: BLOB_TYPE, type_codes.CLOB: CLOB_TYPE, type_codes.NCLOB: NCLOB_TYPE} 47 | 48 | # Bit masks for LOB options (field 2 in header): 49 | LOB_OPTION_ISNULL = 0x01 50 | LOB_OPTION_DATAINCLUDED = 0x02 51 | LOB_OPTION_LASTDATA = 0x04 52 | 53 | OPTIONS_STR = { 54 | LOB_OPTION_ISNULL: 'isnull', 55 | LOB_OPTION_DATAINCLUDED: 'data_included', 56 | LOB_OPTION_LASTDATA: 'last_data' 57 | } 58 | 59 | 60 | class WriteLobHeader(BaseLobheader): 61 | """Write-LOB header structure used when sending data to Hana. 62 | Total header size is 10 bytes. 63 | Note that the lob data does not come immediately after the lob header but AFTER all rowdata headers 64 | have been written to the part header!!! 65 | 66 | 00: TYPECODE: I1 67 | 01: OPTIONS: I1 Options that further refine the descriptor 68 | 02: LENGTH: I4 Length of bytes of data that follows 69 | 06: POSITION: I4 Position P of the lob data in the part (startinb at the beginning of the part) 70 | ... 71 | P: LOB data 72 | """ 73 | header_struct = struct.Struct(' no further data to be read for LOB if options->is_null is true 85 | 02: RESERVED: I2 (ignore this) 86 | 04: CHARLENGTH: I8 Length of string (for asci and unicode) 87 | 12: BYTELENGTH: I8 Number of bytes of LOB 88 | 20: LOCATORID: B8 8 bytes serving as locator id for LOB 89 | 28: CHUNKLENGTH: I4 Number of bytes of LOB chunk in this result set 90 | 32: LOB data if CHUNKLENGTH > 0 91 | """ 92 | header_struct_part1 = struct.Struct('' % value 121 | -------------------------------------------------------------------------------- /tests/test_parts.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | import pytest 16 | from io import BytesIO 17 | ### 18 | from pyhdb.protocol.parts import Part, PART_MAPPING 19 | from pyhdb.exceptions import InterfaceError 20 | 21 | 22 | class DummyPart(Part): 23 | """ 24 | Dummy part definition for testing purposes. 25 | This part contains a defined number of zeros. 26 | """ 27 | 28 | kind = 127 29 | 30 | def __init__(self, zeros=10): 31 | self.zeros = zeros 32 | 33 | def pack_data(self, remaining_size): 34 | return self.zeros, b"\x00" * self.zeros 35 | 36 | @classmethod 37 | def unpack_data(cls, argument_count, payload): 38 | payload = payload.read(argument_count) 39 | assert payload == b"\x00" * argument_count 40 | return argument_count, 41 | 42 | 43 | class TestBasePart(object): 44 | 45 | def test_pack_dummy_part(self): 46 | part = DummyPart(10) 47 | assert part.zeros == 10 48 | 49 | packed = part.pack(0) 50 | header = packed[0:16] 51 | assert header[0:1] == b"\x7f" 52 | assert header[1:2] == b"\x00" 53 | assert header[2:4] == b"\x0a\x00" 54 | assert header[4:8] == b"\x00\x00\x00\x00" 55 | assert header[8:12] == b"\x0a\x00\x00\x00" 56 | assert header[12:16] == b"\x00\x00\x00\x00" 57 | 58 | payload = packed[16:] 59 | assert len(payload) == 16 60 | assert payload == b"\x00" * 16 61 | 62 | def test_unpack_single_dummy_part(self): 63 | packed = BytesIO( 64 | b'\x7F\x00\x0A\x00\x00\x00\x00\x00\x0A\x00\x00\x00\x00' 65 | b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' 66 | ) 67 | 68 | unpacked = tuple(DummyPart.unpack_from(packed, 1)) 69 | assert len(unpacked) == 1 70 | 71 | unpacked = unpacked[0] 72 | assert isinstance(unpacked, DummyPart) 73 | assert unpacked.zeros == 10 74 | 75 | def test_unpack_multiple_dummy_parts(self): 76 | packed = BytesIO( 77 | b"\x7f\x00\x0a\x00\x00\x00\x00\x00\x0a\x00\x00\x00\xc8\xff\x01" 78 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 79 | b"\x00\x00\x7f\x00\x0e\x00\x00\x00\x00\x00\x0e\x00\x00\x00\xa8" 80 | b"\xff\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 81 | b"\x00\x00\x00\x00\x7f\x00\x12\x00\x00\x00\x00\x00\x12\x00\x00" 82 | b"\x00\x68\xff\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 83 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 84 | ) 85 | 86 | unpacked = tuple(Part.unpack_from(packed, 3)) 87 | assert len(unpacked) == 3 88 | assert isinstance(unpacked[0], DummyPart) 89 | assert unpacked[0].zeros == 10 90 | 91 | assert isinstance(unpacked[1], DummyPart) 92 | assert unpacked[1].zeros == 14 93 | 94 | assert isinstance(unpacked[2], DummyPart) 95 | assert unpacked[2].zeros == 18 96 | 97 | def test_invalid_part_header_raises_exception(self): 98 | packed = BytesIO( 99 | b"\xbb\xff\xaa\x00\x00\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x00" 100 | ) 101 | with pytest.raises(InterfaceError): 102 | tuple(Part.unpack_from(packed, 1)) 103 | 104 | def test_unpack_unkown_part_raises_exception(self): 105 | packed = BytesIO( 106 | b"\x80\x00\x0a\x00\x00\x00\x00\x00\x0a\x00\x00\x00\x00" 107 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 108 | ) 109 | 110 | with pytest.raises(InterfaceError): 111 | tuple(Part.unpack_from(packed, 1)) 112 | 113 | 114 | class TestPartMetaClass(object): 115 | 116 | def test_part_kind_mapping(self): 117 | assert 125 not in PART_MAPPING 118 | 119 | class Part125(Part): 120 | kind = 125 121 | assert PART_MAPPING[125] == Part125 122 | 123 | def test_part_without_kind_attribute_will_be_not_in_mapping(self): 124 | assert 123 not in PART_MAPPING 125 | 126 | class Part123(Part): 127 | # No kind attribute 128 | pass 129 | assert 123 not in PART_MAPPING 130 | assert Part123 not in PART_MAPPING.values() 131 | 132 | def test_part_kind_out_of_range_raises_exception(self): 133 | 134 | with pytest.raises(InterfaceError): 135 | class OutOfRangePart(Part): 136 | kind = 255 137 | assert OutOfRangePart not in PART_MAPPING.values() 138 | 139 | def test_part_class_mapping_updates_after_class_left_scope(self): 140 | assert 124 not in PART_MAPPING 141 | 142 | class Part124(Part): 143 | kind = 124 144 | assert PART_MAPPING[124] == Part124 145 | 146 | del Part124 147 | import gc 148 | gc.collect() 149 | 150 | assert 124 not in PART_MAPPING 151 | -------------------------------------------------------------------------------- /tests/test_message.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | import pytest 16 | import mock 17 | from io import BytesIO 18 | ### 19 | from pyhdb.connection import Connection 20 | from pyhdb.protocol.segments import RequestSegment 21 | from pyhdb.protocol.message import RequestMessage, ReplyMessage 22 | 23 | 24 | class DummySegment(RequestSegment): 25 | """Used as pseudo segment instance for some tests""" 26 | @staticmethod 27 | def pack(payload, **kwargs): 28 | payload.write(b"\x00" * 10) 29 | 30 | 31 | class TestRequestRequestMessage(object): 32 | """Test RequestMessage class""" 33 | def test_request_message_init_without_segment(self): 34 | connection = Connection("localhost", 30015, "Fuu", "Bar") 35 | msg = RequestMessage.new(connection) 36 | assert msg.segments == () 37 | 38 | def test_request_message_init_with_single_segment(self): 39 | connection = Connection("localhost", 30015, "Fuu", "Bar") 40 | 41 | request_seg = RequestSegment(0) 42 | msg = RequestMessage.new(connection, request_seg) 43 | assert msg.segments == (request_seg,) 44 | 45 | def test_request_message_init_with_multiple_segments_as_list(self): 46 | connection = Connection("localhost", 30015, "Fuu", "Bar") 47 | 48 | request_seg_1 = RequestSegment(0) 49 | request_seg_2 = RequestSegment(1) 50 | msg = RequestMessage.new(connection, [request_seg_1, request_seg_2]) 51 | assert msg.segments == [request_seg_1, request_seg_2] 52 | 53 | def test_request_message_init_with_multiple_segments_as_tuple(self): 54 | connection = Connection("localhost", 30015, "Fuu", "Bar") 55 | 56 | request_seg_1 = RequestSegment(0) 57 | request_seg_2 = RequestSegment(1) 58 | msg = RequestMessage.new(connection, (request_seg_1, request_seg_2)) 59 | assert msg.segments == (request_seg_1, request_seg_2) 60 | 61 | def test_request_message_use_last_session_id(self): 62 | connection = Connection("localhost", 30015, "Fuu", "Bar") 63 | connection.session_id = 3 64 | 65 | msg1 = RequestMessage.new(connection) 66 | assert msg1.session_id == connection.session_id 67 | 68 | connection.session_id = 5 69 | msg2 = RequestMessage.new(connection) 70 | assert msg2.session_id == connection.session_id 71 | 72 | @mock.patch('pyhdb.connection.Connection.get_next_packet_count', return_value=0) 73 | def test_request_message_keep_packet_count(self, get_next_packet_count): 74 | connection = Connection("localhost", 30015, "Fuu", "Bar") 75 | 76 | msg = RequestMessage.new(connection) 77 | assert msg.packet_count == 0 78 | 79 | # Check two time packet count of the message 80 | # but the get_next_packet_count method of connection 81 | # should only called once. 82 | assert msg.packet_count == 0 83 | get_next_packet_count.assert_called_once_with() 84 | 85 | @pytest.mark.parametrize("autocommit", [False, True]) 86 | def test_payload_pack(self, autocommit): 87 | connection = Connection("localhost", 30015, "Fuu", "Bar", autocommit=autocommit) 88 | 89 | msg = RequestMessage.new(connection, [DummySegment(None)]) 90 | payload = BytesIO() 91 | msg.build_payload(payload) 92 | 93 | assert payload.getvalue() == b"\x00" * 10 94 | 95 | def test_pack(self): 96 | connection = Connection("localhost", 30015, "Fuu", "Bar") 97 | 98 | msg = RequestMessage.new(connection, [DummySegment(None)]) 99 | payload = msg.pack() 100 | packed = payload.getvalue() 101 | assert isinstance(packed, bytes) 102 | 103 | # Session id 104 | assert packed[0:8] == b"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF" 105 | 106 | # Packet count 107 | assert packed[8:12] == b"\x00\x00\x00\x00" 108 | 109 | # var part length 110 | assert packed[12:16] == b"\x0A\x00\x00\x00" 111 | 112 | # var part size 113 | assert packed[16:20] == b"\xE0\xFF\x01\x00" 114 | 115 | # no of segments 116 | assert packed[20:22] == b"\x01\x00" 117 | 118 | # reserved 119 | assert packed[22:32] == b"\x00" * 10 120 | 121 | # payload 122 | assert packed[32:42] == b"\x00" * 10 123 | 124 | 125 | class TestReplyRequestMessage(object): 126 | 127 | def test_message_use_received_session_id(self): 128 | connection = Connection("localhost", 30015, "Fuu", "Bar") 129 | connection.session_id = 12345 130 | msg = ReplyMessage(connection.session_id, connection.get_next_packet_count()) 131 | 132 | assert msg.session_id == 12345 133 | 134 | @mock.patch('pyhdb.connection.Connection.get_next_packet_count', return_value=0) 135 | def test_message_use_received_packet_count(self, get_next_packet_count): 136 | connection = Connection("localhost", 30015, "Fuu", "Bar") 137 | connection.packet_count = 12345 138 | msg = ReplyMessage(connection.session_id, connection.packet_count) 139 | 140 | assert msg.packet_count == 12345 141 | assert not get_next_packet_count.called 142 | -------------------------------------------------------------------------------- /tests/parts/test_options.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | from io import BytesIO 16 | 17 | import pytest 18 | 19 | from pyhdb.exceptions import InterfaceError 20 | from pyhdb.protocol.parts import OptionPart 21 | from pyhdb.protocol import constants 22 | 23 | 24 | def test_not_pack_none_value_items(): 25 | class DummyOptionPart(OptionPart): 26 | kind = 126 27 | 28 | option_definition = { 29 | # Identifier, (Value, Type) 30 | "int_field": (1, 3), 31 | "bigint_field": (2, 4), 32 | "bool_field": (3, 28) 33 | } 34 | 35 | arguments, payload = DummyOptionPart({ 36 | "int_field": 123456789, 37 | "bigint_field": None, 38 | "bool_field": True 39 | }).pack_data(constants.MAX_SEGMENT_SIZE) 40 | assert arguments == 2 41 | 42 | 43 | def test_unknown_option_is_not_packable(): 44 | class DummyOptionPart(OptionPart): 45 | kind = 126 46 | 47 | option_definition = { 48 | # Identifier, (Value, Type) 49 | "int_field": (1, 3), 50 | } 51 | 52 | with pytest.raises(InterfaceError) as excinfo: 53 | DummyOptionPart({ 54 | "unknown_option": 12345 55 | }).pack_data(constants.MAX_SEGMENT_SIZE) 56 | 57 | assert "Unknown option identifier" in excinfo.exconly() 58 | 59 | 60 | class TestOptionPartBooleanType(object): 61 | 62 | class DummyOptionPart(OptionPart): 63 | kind = 126 64 | 65 | option_definition = { 66 | # Identifier, (Value, Type) 67 | "bool_field": (1, 28) 68 | } 69 | 70 | def test_pack_true(self): 71 | arguments, payload = self.DummyOptionPart({ 72 | "bool_field": True 73 | }).pack_data(constants.MAX_SEGMENT_SIZE) 74 | assert arguments == 1 75 | assert payload == b"\x01\x1C\x01" 76 | 77 | def test_pack_false(self): 78 | arguments, payload = self.DummyOptionPart({ 79 | "bool_field": False 80 | }).pack_data(constants.MAX_SEGMENT_SIZE) 81 | assert arguments == 1 82 | assert payload == b"\x01\x1C\x00" 83 | 84 | def test_unpack_true(self): 85 | options, = self.DummyOptionPart.unpack_data( 86 | 1, 87 | BytesIO(b"\x01\x1C\x01") 88 | ) 89 | assert options == {"bool_field": True} 90 | 91 | def test_unpack_false(self): 92 | options, = self.DummyOptionPart.unpack_data( 93 | 1, 94 | BytesIO(b"\x01\x1C\x00") 95 | ) 96 | assert options == {"bool_field": False} 97 | 98 | 99 | class TestOptionPartInt(object): 100 | 101 | class DummyOptionPart(OptionPart): 102 | kind = 126 103 | 104 | option_definition = { 105 | # Identifier, (Value, Type) 106 | "int_field": (1, 3) 107 | } 108 | 109 | def test_pack(self): 110 | arguments, payload = self.DummyOptionPart({ 111 | "int_field": 123456 112 | }).pack_data(constants.MAX_SEGMENT_SIZE) 113 | assert arguments == 1 114 | assert payload == b"\x01\x03\x40\xE2\x01\x00" 115 | 116 | def test_unpack(self): 117 | options, = self.DummyOptionPart.unpack_data( 118 | 1, 119 | BytesIO(b"\x01\x03\x40\xE2\x01\x00") 120 | ) 121 | assert options == {"int_field": 123456} 122 | 123 | 124 | class TestOptionPartBigInt(object): 125 | 126 | class DummyOptionPart(OptionPart): 127 | kind = 126 128 | 129 | option_definition = { 130 | # Identifier, (Value, Type) 131 | "bigint_field": (1, 4) 132 | } 133 | 134 | def test_pack(self): 135 | arguments, payload = self.DummyOptionPart({ 136 | "bigint_field": 2**32 137 | }).pack_data(constants.MAX_SEGMENT_SIZE) 138 | assert arguments == 1 139 | assert payload == b"\x01\x04\x00\x00\x00\x00\x01\x00\x00\x00" 140 | 141 | def test_unpack(self): 142 | options, = self.DummyOptionPart.unpack_data( 143 | 1, 144 | BytesIO(b"\x01\x04\x00\x00\x00\x00\x01\x00\x00\x00") 145 | ) 146 | assert options == {"bigint_field": 2**32} 147 | 148 | 149 | class TestOptionPartString(object): 150 | 151 | class DummyOptionPart(OptionPart): 152 | kind = 126 153 | 154 | option_definition = { 155 | # Identifier, (Value, Type) 156 | "string_field": (1, 29) 157 | } 158 | 159 | def test_pack(self): 160 | arguments, payload = self.DummyOptionPart({ 161 | "string_field": u"Hello World" 162 | }).pack_data(constants.MAX_SEGMENT_SIZE) 163 | assert arguments == 1 164 | assert payload == b"\x01\x1d\x0b\x00\x48\x65\x6c\x6c" \ 165 | b"\x6f\x20\x57\x6f\x72\x6c\x64" 166 | 167 | def test_unpack(self): 168 | options, = self.DummyOptionPart.unpack_data( 169 | 1, 170 | BytesIO( 171 | b"\x01\x1d\x0b\x00\x48\x65\x6c\x6c" 172 | b"\x6f\x20\x57\x6f\x72\x6c\x64" 173 | ) 174 | ) 175 | assert options == {"string_field": u"Hello World"} 176 | -------------------------------------------------------------------------------- /pyhdb/cesu8.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import codecs 16 | from pyhdb.compat import PY2, unichr 17 | 18 | SURROGATE_IDENTICATOR_INT = 0xED 19 | SURROGATE_IDENTICATOR_BYTE = b'\xed' 20 | 21 | 22 | class IncrementalDecoder(codecs.BufferedIncrementalDecoder): 23 | # Decoder inspired by python-ftfy written by Rob Speer 24 | # https://github.com/LuminosoInsight/python-ftfy/blob/master/ftfy/bad_codecs/utf8_variants.py 25 | 26 | def _buffer_decode(self, input, errors, final): 27 | decoded_segments = [] 28 | position = 0 29 | 30 | while True: 31 | decoded, consumed = self._buffer_decode_step(input[position:], errors, final) 32 | 33 | if consumed == 0: 34 | break 35 | 36 | decoded_segments.append(decoded) 37 | position += consumed 38 | 39 | if final and position != len(input): 40 | raise Exception("Final decoder doesn't decoded all bytes") 41 | 42 | return u''.join(decoded_segments), position 43 | 44 | def _buffer_decode_step(self, input, errors, final): 45 | # If begin of CESU-8 sequence 46 | if input.startswith(SURROGATE_IDENTICATOR_BYTE): 47 | if len(input) < 6: 48 | if not final: 49 | # Stream is not done yet 50 | return u'', 0 51 | 52 | # As there are less than six bytes it can't be a CESU-8 surrogate 53 | # but probably a UTF-8 byte sequence 54 | return codecs.utf_8_decode(input, errors, final) 55 | 56 | if PY2: 57 | bytenums = [ord(b) for b in input[:6]] 58 | else: 59 | bytenums = input 60 | 61 | # Verify that the 6 bytes are in possible range of a CESU-8 surrogate 62 | if bytenums[1] >= 0xa0 and bytenums[1] <= 0xbf and \ 63 | bytenums[2] >= 0x80 and bytenums[2] <= 0xbf and \ 64 | bytenums[3] == SURROGATE_IDENTICATOR_INT and \ 65 | bytenums[4] >= 0xb0 and bytenums[4] <= 0xbf and \ 66 | bytenums[5] >= 0x80 and bytenums[5] <= 0xbf: 67 | 68 | codepoint = ( 69 | ((bytenums[1] & 0x0f) << 16) + 70 | ((bytenums[2] & 0x3f) << 10) + 71 | ((bytenums[4] & 0x0f) << 6) + 72 | (bytenums[5] & 0x3f) + 73 | 0x10000 74 | ) 75 | return unichr(codepoint), 6 76 | 77 | # No CESU-8 surrogate but probably a 3 byte UTF-8 sequence 78 | return codecs.utf_8_decode(input[:3], errors, final) 79 | 80 | cesu8_surrogate_start = input.find(SURROGATE_IDENTICATOR_BYTE) 81 | if cesu8_surrogate_start > 0: 82 | # Decode everything until start of cesu8 surrogate pair 83 | return codecs.utf_8_decode(input[:cesu8_surrogate_start], errors, final) 84 | 85 | # No sign of CESU-8 encoding 86 | return codecs.utf_8_decode(input, errors, final) 87 | 88 | 89 | class IncrementalEncoder(codecs.BufferedIncrementalEncoder): 90 | 91 | def _buffer_encode(self, input, errors, final=False): 92 | encoded_segments = [] 93 | position = 0 94 | input_length = len(input) 95 | 96 | while position + 1 <= input_length: 97 | encoded, consumed = self._buffer_encode_step( 98 | input[position], errors, final 99 | ) 100 | 101 | if consumed == 0: 102 | break 103 | 104 | encoded_segments.append(encoded) 105 | position += consumed 106 | 107 | if final and position != len(input): 108 | raise Exception("Final encoder doesn't encode all characters") 109 | 110 | return b''.join(encoded_segments), position 111 | 112 | def _buffer_encode_step(self, char, errors, final): 113 | codepoint = ord(char) 114 | if codepoint <= 65535: 115 | return codecs.utf_8_encode(char, errors) 116 | else: 117 | seq = bytearray(6) 118 | seq[0] = 0xED 119 | seq[1] = 0xA0 | (((codepoint & 0x1F0000) >> 16) - 1) 120 | seq[2] = 0x80 | (codepoint & 0xFC00) >> 10 121 | seq[3] = 0xED 122 | seq[4] = 0xB0 | ((codepoint >> 6) & 0x3F) 123 | seq[5] = 0x80 | (codepoint & 0x3F) 124 | return bytes(seq), 1 125 | 126 | 127 | def encode(input, errors='strict'): 128 | return IncrementalEncoder(errors).encode(input, final=True), len(input) 129 | 130 | 131 | def decode(input, errors='strict'): 132 | return IncrementalDecoder(errors).decode(input, final=True), len(input) 133 | 134 | 135 | class StreamWriter(codecs.StreamWriter): 136 | encode = encode 137 | 138 | 139 | class StreamReader(codecs.StreamReader): 140 | decode = decode 141 | 142 | 143 | CESU8_CODEC_INFO = codecs.CodecInfo( 144 | name="cesu-8", 145 | encode=encode, 146 | decode=decode, 147 | incrementalencoder=IncrementalEncoder, 148 | incrementaldecoder=IncrementalDecoder, 149 | streamreader=StreamReader, 150 | streamwriter=StreamWriter, 151 | ) 152 | 153 | 154 | def search_function(encoding): 155 | if encoding == 'cesu-8': 156 | return CESU8_CODEC_INFO 157 | else: 158 | return None 159 | 160 | codecs.register(search_function) 161 | -------------------------------------------------------------------------------- /pyhdb/protocol/segments.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import io 16 | import struct 17 | import logging 18 | from io import BytesIO 19 | ### 20 | from pyhdb.protocol.constants import part_kinds 21 | from pyhdb.compat import iter_range 22 | from pyhdb.protocol import constants 23 | from pyhdb.protocol.parts import Part 24 | from pyhdb.protocol.headers import RequestSegmentHeader, ReplySegmentHeader 25 | from pyhdb.protocol.constants import segment_kinds 26 | 27 | 28 | logger = logging.getLogger('pyhdb') 29 | debug = logger.debug 30 | 31 | 32 | class BaseSegment(object): 33 | """ 34 | Base class for request and reply segments 35 | """ 36 | base_header_struct_fmt = '' % (self.host, self.port, self.user) 64 | 65 | def _open_socket_and_init_protocoll(self): 66 | self._socket = socket.create_connection((self.host, self.port), self._timeout) 67 | 68 | # Initialization Handshake 69 | self._socket.sendall(INITIALIZATION_BYTES) 70 | 71 | response = self._socket.recv(8) 72 | if len(response) != 8: 73 | raise Exception("Connection failed") 74 | 75 | self.product_version = version_struct.unpack(response[0:3]) 76 | self.protocol_version = version_struct.unpack_from(response[3:8]) 77 | 78 | def send_request(self, message): 79 | """Send message request to HANA db and return reply message 80 | :param message: Instance of Message object containing segments and parts of a HANA db request 81 | :returns: Instance of reply Message object 82 | """ 83 | payload = message.pack() # obtain BytesIO instance 84 | return self.__send_message_recv_reply(payload.getvalue()) 85 | 86 | def __send_message_recv_reply(self, packed_message): 87 | """ 88 | Private method to send packed message and receive the reply message. 89 | :param packed_message: a binary string containing the entire message payload 90 | """ 91 | payload = io.BytesIO() 92 | try: 93 | with self._socket_lock: 94 | self._socket.sendall(packed_message) 95 | 96 | # Read first message header 97 | raw_header = self._socket.recv(32) 98 | header = ReplyMessage.header_from_raw_header_data(raw_header) 99 | 100 | # from pyhdb.lib.stringlib import allhexlify 101 | # print 'Raw msg header:', allhexlify(raw_header) 102 | msg = 'Message header (32 bytes): sessionid: %d, packetcount: %d, length: %d, size: %d, noofsegm: %d' 103 | debug(msg, *(header[:5])) 104 | 105 | # Receive complete message payload 106 | while payload.tell() < header.payload_length: 107 | _payload = self._socket.recv(header.payload_length - payload.tell()) 108 | if not _payload: 109 | break # jump out without any warning?? 110 | payload.write(_payload) 111 | 112 | debug('Read %d bytes payload from socket', payload.tell()) 113 | 114 | # Keep session id of connection up to date 115 | if self.session_id != header.session_id: 116 | self.session_id = header.session_id 117 | self.packet_count = -1 118 | except socket.timeout: 119 | raise ConnectionTimedOutError() 120 | except (IOError, OSError) as error: 121 | raise OperationalError("Lost connection to HANA server (%r)" % error) 122 | 123 | payload.seek(0) # set pointer position to beginning of buffer 124 | return ReplyMessage.unpack_reply(header, payload) 125 | 126 | def get_next_packet_count(self): 127 | with self._packet_count_lock: 128 | self.packet_count += 1 129 | return self.packet_count 130 | 131 | def connect(self): 132 | with self._socket_lock: 133 | if self._socket is not None: 134 | # Socket already established 135 | return 136 | 137 | self._open_socket_and_init_protocoll() 138 | 139 | # Perform the authenication handshake and get the part 140 | # with the agreed authentication data 141 | agreed_auth_part = self._auth_manager.perform_handshake() 142 | 143 | request = RequestMessage.new( 144 | self, 145 | RequestSegment( 146 | message_types.CONNECT, 147 | ( 148 | agreed_auth_part, 149 | ClientId( 150 | "pyhdb-%s@%s" % (os.getpid(), socket.getfqdn()) 151 | ), 152 | ConnectOptions(DEFAULT_CONNECTION_OPTIONS) 153 | ) 154 | ) 155 | ) 156 | self.send_request(request) 157 | 158 | def close(self): 159 | with self._socket_lock: 160 | if self._socket is None: 161 | raise Error("Connection already closed") 162 | 163 | try: 164 | request = RequestMessage.new( 165 | self, 166 | RequestSegment(message_types.DISCONNECT) 167 | ) 168 | reply = self.send_request(request) 169 | if reply.segments[0].function_code != \ 170 | function_codes.DISCONNECT: 171 | raise Error("Connection wasn't closed correctly") 172 | finally: 173 | self._socket.close() 174 | self._socket = None 175 | 176 | @property 177 | def closed(self): 178 | return self._socket is None 179 | 180 | def _check_closed(self): 181 | if self.closed: 182 | raise Error("Connection closed") 183 | 184 | def cursor(self): 185 | """Return a new Cursor Object using the connection.""" 186 | self._check_closed() 187 | 188 | return Cursor(self) 189 | 190 | def commit(self): 191 | self._check_closed() 192 | 193 | request = RequestMessage.new( 194 | self, 195 | RequestSegment(message_types.COMMIT) 196 | ) 197 | self.send_request(request) 198 | 199 | def rollback(self): 200 | self._check_closed() 201 | 202 | request = RequestMessage.new( 203 | self, 204 | RequestSegment(message_types.ROLLBACK) 205 | ) 206 | self.send_request(request) 207 | 208 | @property 209 | def timeout(self): 210 | if self._socket: 211 | return self._socket.gettimeout() 212 | return self._timeout 213 | 214 | @timeout.setter 215 | def timeout(self, value): 216 | self._timeout = value 217 | if self._socket: 218 | self._socket.settimeout(value) 219 | 220 | # Methods for compatibility with hdbclient 221 | def getautocommit(self): 222 | return self.autocommit 223 | 224 | def setautocommit(self, auto=True): 225 | self.autocommit = auto 226 | 227 | def isconnected(self): 228 | return self._socket is not None 229 | -------------------------------------------------------------------------------- /tests/parts/test_fields.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | from io import BytesIO 16 | from pyhdb.protocol.parts import Fields 17 | 18 | 19 | def test_pack_data(): 20 | packed = Fields.pack_data(["Hello", "World"]) 21 | assert packed == \ 22 | b"\x02\x00\x05\x48\x65\x6c\x6c\x6f\x05\x57\x6f\x72\x6c\x64" 23 | 24 | 25 | def test_unpack_data(): 26 | packed = BytesIO( 27 | b"\x02\x00\x05\x48\x65\x6c\x6c\x6f\x05\x57\x6f\x72\x6c\x64" 28 | ) 29 | unpacked = Fields.unpack_data(packed) 30 | assert unpacked == [b"Hello", b"World"] 31 | 32 | 33 | def test_pack_large_data(): 34 | packed = Fields.pack_data([ 35 | b"97f0f004be65439846e0eae3e67edacbaa6e578d1e8ba1e3d2f57e18460967d1" 36 | b"433bd920e7e9221c4a4631f59730096f73f8df748b990c24dec2714ba8ade446" 37 | b"28eeffe47b54447c452f1bebdc6a21e00f576daca1ec2c1f991fc3c465c7b493" 38 | b"900e8c8bc79b772f47802d2fb7424dec7aae835c2802974802e5a4a1b79dcb63" 39 | b"a7c18846a1171d8e2150ce804b68a7db02810a058159", 40 | b"e7d536e3f67ce32a6e4a439880d28010df2199459b4e2836e272fba1d8597479" 41 | b"ff76db462267029601579310a36e49b2bc34aade017f57e4d40f110abea1a1bd" 42 | b"f4a17a1e20f28fe3751e83ffd3dc383b6e965e3a9f5d28d4378d31fa70dda065" 43 | b"1fa09ab1fc3a817148da42b3dcbeb4264d1ec6a7385abf3b9598459b337bbf6a" 44 | b"41fb49769e20735e5842fcb1e3ee1d19bfd2e7e249f5" 45 | ]) 46 | assert packed == \ 47 | b"\x02\x00\xFF\x2C\x01\x39\x37\x66\x30\x66\x30\x30\x34\x62\x65\x36" \ 48 | b"\x35\x34\x33\x39\x38\x34\x36\x65\x30\x65\x61\x65\x33\x65\x36\x37" \ 49 | b"\x65\x64\x61\x63\x62\x61\x61\x36\x65\x35\x37\x38\x64\x31\x65\x38" \ 50 | b"\x62\x61\x31\x65\x33\x64\x32\x66\x35\x37\x65\x31\x38\x34\x36\x30" \ 51 | b"\x39\x36\x37\x64\x31\x34\x33\x33\x62\x64\x39\x32\x30\x65\x37\x65" \ 52 | b"\x39\x32\x32\x31\x63\x34\x61\x34\x36\x33\x31\x66\x35\x39\x37\x33" \ 53 | b"\x30\x30\x39\x36\x66\x37\x33\x66\x38\x64\x66\x37\x34\x38\x62\x39" \ 54 | b"\x39\x30\x63\x32\x34\x64\x65\x63\x32\x37\x31\x34\x62\x61\x38\x61" \ 55 | b"\x64\x65\x34\x34\x36\x32\x38\x65\x65\x66\x66\x65\x34\x37\x62\x35" \ 56 | b"\x34\x34\x34\x37\x63\x34\x35\x32\x66\x31\x62\x65\x62\x64\x63\x36" \ 57 | b"\x61\x32\x31\x65\x30\x30\x66\x35\x37\x36\x64\x61\x63\x61\x31\x65" \ 58 | b"\x63\x32\x63\x31\x66\x39\x39\x31\x66\x63\x33\x63\x34\x36\x35\x63" \ 59 | b"\x37\x62\x34\x39\x33\x39\x30\x30\x65\x38\x63\x38\x62\x63\x37\x39" \ 60 | b"\x62\x37\x37\x32\x66\x34\x37\x38\x30\x32\x64\x32\x66\x62\x37\x34" \ 61 | b"\x32\x34\x64\x65\x63\x37\x61\x61\x65\x38\x33\x35\x63\x32\x38\x30" \ 62 | b"\x32\x39\x37\x34\x38\x30\x32\x65\x35\x61\x34\x61\x31\x62\x37\x39" \ 63 | b"\x64\x63\x62\x36\x33\x61\x37\x63\x31\x38\x38\x34\x36\x61\x31\x31" \ 64 | b"\x37\x31\x64\x38\x65\x32\x31\x35\x30\x63\x65\x38\x30\x34\x62\x36" \ 65 | b"\x38\x61\x37\x64\x62\x30\x32\x38\x31\x30\x61\x30\x35\x38\x31\x35" \ 66 | b"\x39\xFF\x2C\x01\x65\x37\x64\x35\x33\x36\x65\x33\x66\x36\x37\x63" \ 67 | b"\x65\x33\x32\x61\x36\x65\x34\x61\x34\x33\x39\x38\x38\x30\x64\x32" \ 68 | b"\x38\x30\x31\x30\x64\x66\x32\x31\x39\x39\x34\x35\x39\x62\x34\x65" \ 69 | b"\x32\x38\x33\x36\x65\x32\x37\x32\x66\x62\x61\x31\x64\x38\x35\x39" \ 70 | b"\x37\x34\x37\x39\x66\x66\x37\x36\x64\x62\x34\x36\x32\x32\x36\x37" \ 71 | b"\x30\x32\x39\x36\x30\x31\x35\x37\x39\x33\x31\x30\x61\x33\x36\x65" \ 72 | b"\x34\x39\x62\x32\x62\x63\x33\x34\x61\x61\x64\x65\x30\x31\x37\x66" \ 73 | b"\x35\x37\x65\x34\x64\x34\x30\x66\x31\x31\x30\x61\x62\x65\x61\x31" \ 74 | b"\x61\x31\x62\x64\x66\x34\x61\x31\x37\x61\x31\x65\x32\x30\x66\x32" \ 75 | b"\x38\x66\x65\x33\x37\x35\x31\x65\x38\x33\x66\x66\x64\x33\x64\x63" \ 76 | b"\x33\x38\x33\x62\x36\x65\x39\x36\x35\x65\x33\x61\x39\x66\x35\x64" \ 77 | b"\x32\x38\x64\x34\x33\x37\x38\x64\x33\x31\x66\x61\x37\x30\x64\x64" \ 78 | b"\x61\x30\x36\x35\x31\x66\x61\x30\x39\x61\x62\x31\x66\x63\x33\x61" \ 79 | b"\x38\x31\x37\x31\x34\x38\x64\x61\x34\x32\x62\x33\x64\x63\x62\x65" \ 80 | b"\x62\x34\x32\x36\x34\x64\x31\x65\x63\x36\x61\x37\x33\x38\x35\x61" \ 81 | b"\x62\x66\x33\x62\x39\x35\x39\x38\x34\x35\x39\x62\x33\x33\x37\x62" \ 82 | b"\x62\x66\x36\x61\x34\x31\x66\x62\x34\x39\x37\x36\x39\x65\x32\x30" \ 83 | b"\x37\x33\x35\x65\x35\x38\x34\x32\x66\x63\x62\x31\x65\x33\x65\x65" \ 84 | b"\x31\x64\x31\x39\x62\x66\x64\x32\x65\x37\x65\x32\x34\x39\x66\x35" 85 | 86 | 87 | def test_unpack_large_data(): 88 | packed = BytesIO( 89 | b"\x02\x00\xFF\x2C\x01\x39\x37\x66\x30\x66\x30\x30\x34\x62\x65\x36" 90 | b"\x35\x34\x33\x39\x38\x34\x36\x65\x30\x65\x61\x65\x33\x65\x36\x37" 91 | b"\x65\x64\x61\x63\x62\x61\x61\x36\x65\x35\x37\x38\x64\x31\x65\x38" 92 | b"\x62\x61\x31\x65\x33\x64\x32\x66\x35\x37\x65\x31\x38\x34\x36\x30" 93 | b"\x39\x36\x37\x64\x31\x34\x33\x33\x62\x64\x39\x32\x30\x65\x37\x65" 94 | b"\x39\x32\x32\x31\x63\x34\x61\x34\x36\x33\x31\x66\x35\x39\x37\x33" 95 | b"\x30\x30\x39\x36\x66\x37\x33\x66\x38\x64\x66\x37\x34\x38\x62\x39" 96 | b"\x39\x30\x63\x32\x34\x64\x65\x63\x32\x37\x31\x34\x62\x61\x38\x61" 97 | b"\x64\x65\x34\x34\x36\x32\x38\x65\x65\x66\x66\x65\x34\x37\x62\x35" 98 | b"\x34\x34\x34\x37\x63\x34\x35\x32\x66\x31\x62\x65\x62\x64\x63\x36" 99 | b"\x61\x32\x31\x65\x30\x30\x66\x35\x37\x36\x64\x61\x63\x61\x31\x65" 100 | b"\x63\x32\x63\x31\x66\x39\x39\x31\x66\x63\x33\x63\x34\x36\x35\x63" 101 | b"\x37\x62\x34\x39\x33\x39\x30\x30\x65\x38\x63\x38\x62\x63\x37\x39" 102 | b"\x62\x37\x37\x32\x66\x34\x37\x38\x30\x32\x64\x32\x66\x62\x37\x34" 103 | b"\x32\x34\x64\x65\x63\x37\x61\x61\x65\x38\x33\x35\x63\x32\x38\x30" 104 | b"\x32\x39\x37\x34\x38\x30\x32\x65\x35\x61\x34\x61\x31\x62\x37\x39" 105 | b"\x64\x63\x62\x36\x33\x61\x37\x63\x31\x38\x38\x34\x36\x61\x31\x31" 106 | b"\x37\x31\x64\x38\x65\x32\x31\x35\x30\x63\x65\x38\x30\x34\x62\x36" 107 | b"\x38\x61\x37\x64\x62\x30\x32\x38\x31\x30\x61\x30\x35\x38\x31\x35" 108 | b"\x39\xFF\x2C\x01\x65\x37\x64\x35\x33\x36\x65\x33\x66\x36\x37\x63" 109 | b"\x65\x33\x32\x61\x36\x65\x34\x61\x34\x33\x39\x38\x38\x30\x64\x32" 110 | b"\x38\x30\x31\x30\x64\x66\x32\x31\x39\x39\x34\x35\x39\x62\x34\x65" 111 | b"\x32\x38\x33\x36\x65\x32\x37\x32\x66\x62\x61\x31\x64\x38\x35\x39" 112 | b"\x37\x34\x37\x39\x66\x66\x37\x36\x64\x62\x34\x36\x32\x32\x36\x37" 113 | b"\x30\x32\x39\x36\x30\x31\x35\x37\x39\x33\x31\x30\x61\x33\x36\x65" 114 | b"\x34\x39\x62\x32\x62\x63\x33\x34\x61\x61\x64\x65\x30\x31\x37\x66" 115 | b"\x35\x37\x65\x34\x64\x34\x30\x66\x31\x31\x30\x61\x62\x65\x61\x31" 116 | b"\x61\x31\x62\x64\x66\x34\x61\x31\x37\x61\x31\x65\x32\x30\x66\x32" 117 | b"\x38\x66\x65\x33\x37\x35\x31\x65\x38\x33\x66\x66\x64\x33\x64\x63" 118 | b"\x33\x38\x33\x62\x36\x65\x39\x36\x35\x65\x33\x61\x39\x66\x35\x64" 119 | b"\x32\x38\x64\x34\x33\x37\x38\x64\x33\x31\x66\x61\x37\x30\x64\x64" 120 | b"\x61\x30\x36\x35\x31\x66\x61\x30\x39\x61\x62\x31\x66\x63\x33\x61" 121 | b"\x38\x31\x37\x31\x34\x38\x64\x61\x34\x32\x62\x33\x64\x63\x62\x65" 122 | b"\x62\x34\x32\x36\x34\x64\x31\x65\x63\x36\x61\x37\x33\x38\x35\x61" 123 | b"\x62\x66\x33\x62\x39\x35\x39\x38\x34\x35\x39\x62\x33\x33\x37\x62" 124 | b"\x62\x66\x36\x61\x34\x31\x66\x62\x34\x39\x37\x36\x39\x65\x32\x30" 125 | b"\x37\x33\x35\x65\x35\x38\x34\x32\x66\x63\x62\x31\x65\x33\x65\x65" 126 | b"\x31\x64\x31\x39\x62\x66\x64\x32\x65\x37\x65\x32\x34\x39\x66\x35" 127 | ) 128 | unpacked = Fields.unpack_data(packed) 129 | assert unpacked == [ 130 | b"97f0f004be65439846e0eae3e67edacbaa6e578d1e8ba1e3d2f57e18460967d1" 131 | b"433bd920e7e9221c4a4631f59730096f73f8df748b990c24dec2714ba8ade446" 132 | b"28eeffe47b54447c452f1bebdc6a21e00f576daca1ec2c1f991fc3c465c7b493" 133 | b"900e8c8bc79b772f47802d2fb7424dec7aae835c2802974802e5a4a1b79dcb63" 134 | b"a7c18846a1171d8e2150ce804b68a7db02810a058159", 135 | b"e7d536e3f67ce32a6e4a439880d28010df2199459b4e2836e272fba1d8597479" 136 | b"ff76db462267029601579310a36e49b2bc34aade017f57e4d40f110abea1a1bd" 137 | b"f4a17a1e20f28fe3751e83ffd3dc383b6e965e3a9f5d28d4378d31fa70dda065" 138 | b"1fa09ab1fc3a817148da42b3dcbeb4264d1ec6a7385abf3b9598459b337bbf6a" 139 | b"41fb49769e20735e5842fcb1e3ee1d19bfd2e7e249f5" 140 | ] 141 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | SAP HANA Database Client for Python 2 | =================================== 3 | 4 | Important Notice 5 | ---------------- 6 | 7 | .. image:: https://img.shields.io/badge/STATUS-NOT%20CURRENTLY%20MAINTAINED-red.svg?longCache=true&style=flat 8 | 9 | 10 | This public repository is read-only and no longer maintained. 11 | 12 | The active maintained alternative is SAP HANA Python Client: https://pypi.org/project/hdbcli/ 13 | 14 | A pure Python client for the SAP HANA Database based on the `SAP HANA Database SQL Command Network Protocol `_. 15 | 16 | pyhdb supports Python 2.7, 3.3, 3.4, 3.5, 3.6 and also PyPy on Linux, OSX and Windows. It implements a large part of the `DBAPI Specification v2.0 (PEP 249) `_. 17 | 18 | Table of contents 19 | ----------------- 20 | 21 | * `Install <#install>`_ 22 | * `Getting started <#getting-started>`_ 23 | * `Establish a database connection <#establish-a-database-connection>`_ 24 | * `Cursor object <#cursor-object>`_ 25 | * `Large Objects (LOBs) <#lobs>`_ 26 | * `Stored Procedures <#stored-procedures>`_ 27 | * `Transaction handling <#transaction-handling>`_ 28 | * `Contribute <#contribute>`_ 29 | 30 | Install 31 | ------- 32 | 33 | Install from Python Package Index: 34 | 35 | .. code-block:: bash 36 | 37 | $ pip install pyhdb 38 | 39 | Install from GitHub via pip: 40 | 41 | .. code-block:: bash 42 | 43 | $ pip install git+https://github.com/SAP/pyhdb.git 44 | 45 | You can also install the latest version direct from a cloned git repository. 46 | 47 | .. code-block:: bash 48 | 49 | $ git clone https://github.com/SAP/pyhdb.git 50 | $ cd pyhdb 51 | $ python setup.py install 52 | 53 | 54 | Getting started 55 | --------------- 56 | 57 | If you do not have access to a SAP HANA server, go to the `SAP HANA Developer Center `_ and choose one of the options to `get your own trial SAP HANA Server `_. 58 | 59 | For using PyHDB with hanatrial instance, follow `this guide `_. 60 | 61 | The basic pyhdb usage is common to database adapters implementing the `DBAPI 2.0 interface (PEP 249) `_. The following example shows how easy it's to use the pyhdb module. 62 | 63 | .. code-block:: pycon 64 | 65 | >>> import pyhdb 66 | >>> connection = pyhdb.connect( 67 | host="example.com", 68 | port=30015, 69 | user="user", 70 | password="secret" 71 | ) 72 | 73 | >>> cursor = connection.cursor() 74 | >>> cursor.execute("SELECT 'Hello Python World' FROM DUMMY") 75 | >>> cursor.fetchone() 76 | (u"Hello Python World",) 77 | 78 | >>> connection.close() 79 | 80 | Establish a database connection 81 | ------------------------------- 82 | 83 | The function ``pyhdb.connect`` creates a new database session and returns a new ``Connection`` instance. 84 | Please note that port isn't the instance number of you SAP HANA database. The SQL port of your SAP 85 | HANA is made up of ``315`` for example the port of the default instance number ``00`` is ``30015``. 86 | 87 | Currently pyhdb only supports the user and password authentication method. If you need another 88 | authentication method like SAML or Kerberos than please open a GitHub issue. Also there is currently 89 | no support of encrypted network communication between client and database. 90 | 91 | Cursor object 92 | ------------- 93 | 94 | With the method ``cursor`` of your ``Connection`` object you create a new ``Cursor`` object. 95 | This object is able to execute SQL statements and fetch one or multiple rows of the resultset from the database. 96 | 97 | Example select 98 | ^^^^^^^^^^^^^^ 99 | 100 | .. code-block:: pycon 101 | 102 | >>> cursor = connection.cursor() 103 | >>> cursor.execute("SELECT SCHEMA_NAME, TABLE_NAME FROM TABLES") 104 | 105 | 106 | After you executed a statement you can fetch one or multiple rows from the resultset. 107 | 108 | 109 | .. code-block:: pycon 110 | 111 | >>> cursor.fetchone() 112 | (u'SYS', u'DUMMY') 113 | 114 | >>> cursor.fetchmany(3) 115 | [(u'SYS', u'DUMMY'), (u'SYS', u'PROCEDURE_DATAFLOWS'), (u'SYS', u'PROCEDURE_MAPPING')] 116 | 117 | You can also fetch all rows from your resultset. 118 | 119 | .. code-block:: pycon 120 | 121 | >>> cursor.fetchall() 122 | [(u'SYS', u'DUMMY'), (u'SYS', u'PROCEDURE_DATAFLOWS'), (u'SYS', u'PROCEDURE_MAPPING'), ...] 123 | 124 | 125 | Example Create table 126 | ^^^^^^^^^^^^^^^^^^^^ 127 | 128 | With the execute method you can also execute DDL statements like ``CREATE TABLE``. 129 | 130 | .. code-block:: pycon 131 | 132 | >>> cursor.execute('CREATE TABLE PYHDB_TEST("NAMES" VARCHAR (255) null)') 133 | 134 | 135 | Example insert 136 | ^^^^^^^^^^^^^^ 137 | 138 | You can also execute DML Statements with the execute method like ``INSERT`` or ``DELETE``. The Cursor 139 | attribute ``rowcount`` contains the number of affected rows by the last statement. 140 | 141 | .. code-block:: pycon 142 | 143 | >>> cursor.execute("INSERT INTO PYHDB_TEST VALUES('Hello Python World')") 144 | >>> cursor.rowcount 145 | 1 146 | 147 | 148 | LOBs 149 | ^^^^ 150 | 151 | Three different types of LOBs are supported and corresponding LOB classes have been implemented: 152 | * Blob - binary LOB data 153 | * Clob - string LOB data containing only ascii characters 154 | * NClob - string (unicode for Python 2.x) LOB data containing any valid unicode character 155 | 156 | LOB instance provide a file-like interface (similar to StringIO instances) for accessing LOB data. 157 | For HANA LOBs lazy loading of the actual data is implemented behind the scenes. An initial select statement for a LOB 158 | only loads the first 1024 bytes on the client: 159 | 160 | .. code-block:: pycon 161 | 162 | >>> mylob = cursor.execute('select myclob from mytable where id=:1', [some_id]).fetchone()[0] 163 | >>> mylob 164 | 165 | 166 | By calling the read()-method more data will be loaded from the database once exceeds the number 167 | of currently loaded data: 168 | 169 | .. code-block:: pycon 170 | 171 | >>> myload.read(1500) # -> returns the first 1500 chars, by loading extra 476 chars from the db 172 | >>> mylob 173 | 174 | >>> myload.read() # -> returns the last 500 chars by loading them from the db 175 | >>> mylob 176 | 177 | 178 | Using the ``seek()`` methods it is possible to point the file pointer position within the LOB to arbitrary positions. 179 | ``tell()`` will return the current position. 180 | 181 | 182 | LOBs can be written to the database via ``insert`` or ``update``-statemens with LOB data provided either 183 | as strings or LOB instances: 184 | 185 | .. code-block:: pycon 186 | 187 | >>> from pyhdb import NClob 188 | >>> nclob_data = u'朱の子ましける日におえつかうまつ' 189 | >>> nclob = NClob(nclob_data) 190 | >>> cursor.execute('update mynclob set nclob_1=:1, nclob_2=:2 where id=:3', [nclob, nclob_data, myid]) 191 | 192 | .. note:: Currently LOBs can only be written in the database for sizes up to 128k (entire amount of data provided in one 193 | ``update`` or ``insert`` statement). This constraint will be removed in one of the next releases of PyHDB. 194 | This limitation does however not apply when reading LOB data from the database. 195 | 196 | 197 | Stored Procedures 198 | ^^^^^^^^^^^^^^^^^ 199 | 200 | Rudimentary support for Stored Procedures call, scalar parameters only: 201 | 202 | The script shall call the stored procedure PROC_ADD2 (source below): 203 | 204 | .. code-block:: pycon 205 | 206 | >>> sql_to_prepare = 'call PROC_ADD2 (?, ?, ?, ?)' 207 | >>> params = {'A':2, 'B':5, 'C':None, 'D': None} 208 | >>> psid = cursor.prepare(sql_to_prepare) 209 | >>> ps = cursor.get_prepared_statement(psid) 210 | >>> cursor.execute_prepared(ps, [params]) 211 | >>> result = cursor.fetchall() 212 | >>> for l in result: 213 | >>> print l 214 | 215 | from the stored procedure: 216 | 217 | .. code-block:: sql 218 | 219 | create procedure PROC_ADD2 (in a int, in b int, out c int, out d char) 220 | language sqlscript 221 | reads sql data as 222 | begin 223 | c := :a + :b; 224 | d := 'A'; 225 | end 226 | 227 | Transaction handling 228 | -------------------- 229 | 230 | Please note that all cursors created from the same connection are not isolated. Any change done by one 231 | cursor is immediately visible to all other cursors from same connection. Cursors created from different 232 | connections are isolated as the connection based on the normal transaction handling. 233 | 234 | The connection objects provides to method ``commit`` which commit any pending transaction of the 235 | connection. The method ``rollback`` undo all changes since the last commit. 236 | 237 | Contribute 238 | ---------- 239 | 240 | If you found bugs or have other issues than you are welcome to create a GitHub Issue. If you have 241 | questions about usage or something similar please create a `Stack Overflow `_ 242 | Question with tag `pyhdb `_. 243 | 244 | Run tests 245 | ^^^^^^^^^ 246 | 247 | pyhdb provides a test suite which covers the most use-cases and protocol parts. To run the test suite 248 | you need the ``pytest`` and ``mock`` package. Afterwards just run ``py.test`` inside of the root 249 | directory of the repository. 250 | 251 | .. code-block:: bash 252 | 253 | $ pip install pytest mock 254 | $ py.test 255 | 256 | You can also test different python version with ``tox``. 257 | 258 | .. code-block:: bash 259 | 260 | $ pip install tox 261 | $ tox 262 | 263 | Tracing 264 | ^^^^^^^ 265 | 266 | For debugging purposes it is sometimes useful to get detailed tracing information about packages sent to hana and 267 | those received from the database. There are two ways to turn on the print out of tracing information: 268 | 269 | 1. Set the environment variable HDB_TRACING=1 before starting Python, e.g. (bash-syntax!): 270 | 271 | .. code-block:: bash 272 | 273 | $ HDB_TRACE=1 python 274 | 275 | 2. Import the pyhdb module and set ``pyhdb.tracing = True`` 276 | 277 | Then perform some statements on the database and enjoy the output. 278 | 279 | To get tracing information when running pytest provide the ``-s`` option: 280 | 281 | .. code-block:: bash 282 | 283 | $ HDB_TRACE=1 py.test -s 284 | 285 | 286 | ToDos 287 | ^^^^^ 288 | 289 | * Allow execution of stored database procedure 290 | * Support of ``SELECT FOR UPDATE`` 291 | * Authentication methods 292 | 293 | * SAML 294 | * Kerberos 295 | -------------------------------------------------------------------------------- /tests/test_cursor.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | import pytest 16 | from decimal import Decimal 17 | 18 | from pyhdb.cursor import format_operation 19 | from pyhdb.exceptions import ProgrammingError, IntegrityError 20 | import tests.helper 21 | 22 | TABLE = 'PYHDB_TEST_1' 23 | TABLE_FIELDS = 'TEST VARCHAR(255)' 24 | 25 | 26 | @pytest.fixture 27 | def test_table_1(request, connection): 28 | """Fixture to create table for testing, and dropping it after test run""" 29 | tests.helper.create_table_fixture(request, connection, TABLE, TABLE_FIELDS) 30 | 31 | @pytest.fixture 32 | def test_table_2(request, connection): 33 | """Fixture to create table for testing, and dropping it after test run""" 34 | tests.helper.create_table_fixture(request, connection, 'PYHDB_TEST_2', 'TEST DECIMAL') 35 | 36 | @pytest.fixture 37 | def content_table_1(request, connection): 38 | """Additional fixture to test_table_1, inserts some rows for testing""" 39 | cursor = connection.cursor() 40 | cursor.execute("insert into PYHDB_TEST_1 values('row1')") 41 | cursor.execute("insert into PYHDB_TEST_1 values('row2')") 42 | cursor.execute("insert into PYHDB_TEST_1 values('row3')") 43 | 44 | 45 | @pytest.mark.parametrize("parameters", [ 46 | None, 47 | (), 48 | [] 49 | ]) 50 | def test_format_operation_without_parameters(parameters): 51 | """Test that providing no parameter produces correct result.""" 52 | operation = "SELECT * FROM TEST WHERE fuu = 'bar'" 53 | assert format_operation(operation, parameters) == operation 54 | 55 | 56 | def test_format_operation_with_positional_parameters(): 57 | """Test that correct number of parameters produces correct result.""" 58 | assert format_operation( 59 | "INSERT INTO TEST VALUES(%s, %s)", ('Hello World', 2) 60 | ) == "INSERT INTO TEST VALUES('Hello World', 2)" 61 | 62 | 63 | def test_format_operation_with_too_few_positional_parameters_raises(): 64 | """Test that providing too few parameters raises exception""" 65 | with pytest.raises(ProgrammingError): 66 | format_operation("INSERT INTO TEST VALUES(%s, %s)", ('Hello World',)) 67 | 68 | 69 | def test_format_operation_with_too_many_positional_parameters_raises(): 70 | """Test that providing too many parameters raises exception""" 71 | with pytest.raises(ProgrammingError): 72 | format_operation("INSERT INTO TEST VALUES(%s)", ('Hello World', 2)) 73 | 74 | 75 | def test_format_operation_with_named_parameters(): 76 | """format_operation() is used for Python style parameter expansion""" 77 | assert format_operation( 78 | "INSERT INTO TEST VALUES(%(name)s, %(val)s)", 79 | {'name': 'Hello World', 'val': 2} 80 | ) == "INSERT INTO TEST VALUES('Hello World', 2)" 81 | 82 | 83 | @pytest.mark.hanatest 84 | def test_cursor_fetch_without_execution(connection): 85 | cursor = connection.cursor() 86 | with pytest.raises(ProgrammingError): 87 | cursor.fetchone() 88 | 89 | 90 | @pytest.mark.hanatest 91 | def test_cursor_fetchall_single_row(connection): 92 | cursor = connection.cursor() 93 | cursor.execute("SELECT 1 FROM DUMMY") 94 | 95 | result = cursor.fetchall() 96 | assert result == [(1,)] 97 | 98 | 99 | @pytest.mark.hanatest 100 | def test_cursor_fetchall_multiple_rows(connection): 101 | cursor = connection.cursor() 102 | cursor.execute('SELECT "VIEW_NAME" FROM "PUBLIC"."VIEWS" LIMIT 10') 103 | 104 | result = cursor.fetchall() 105 | assert len(result) == 10 106 | 107 | 108 | # Test cases for different parameter style expansion 109 | # 110 | # paramstyle Meaning 111 | # --------------------------------------------------------- 112 | # 1) qmark Question mark style, e.g. ...WHERE name=? 113 | # 2) numeric Numeric, positional style, e.g. ...WHERE name=:1 114 | # 3) named Named style, e.g. ...WHERE name=:name -> NOT IMPLEMENTED !! 115 | # 4) format ANSI C printf format codes, e.g. ...WHERE name=%s 116 | # 5) pyformat Python extended format codes, e.g. ...WHERE name=%(name)s 117 | 118 | @pytest.mark.hanatest 119 | def test_cursor_execute_with_params1(connection, test_table_1, content_table_1): 120 | """Test qmark parameter expansion style - uses cursor.prepare*() methods""" 121 | # Note: use fetchall() to check that only one row gets returned 122 | cursor = connection.cursor() 123 | 124 | sql = 'select test from PYHDB_TEST_1 where test=?' 125 | # correct way: 126 | assert cursor.execute(sql, ['row2']).fetchall() == [('row2',)] 127 | # invalid - extra unexpected parameter 128 | with pytest.raises(ProgrammingError): 129 | cursor.execute(sql, ['row2', 'extra']).fetchall() 130 | 131 | 132 | @pytest.mark.hanatest 133 | def test_cursor_execute_with_params2(connection, test_table_1, content_table_1): 134 | """Test numeric parameter expansion style - uses cursor.prepare() methods""" 135 | # Note: use fetchall() to check that only one row gets returned 136 | cursor = connection.cursor() 137 | 138 | sql = 'select test from PYHDB_TEST_1 where test=?' 139 | # correct way: 140 | assert cursor.execute(sql, ['row2']).fetchall() == [('row2',)] 141 | # invalid - extra unexpected parameter 142 | with pytest.raises(ProgrammingError): 143 | cursor.execute(sql, ['row2', 'extra']).fetchall() 144 | 145 | 146 | @pytest.mark.hanatest 147 | def test_cursor_execute_with_params4(connection, test_table_1, content_table_1): 148 | """Test format (positional) parameter expansion style""" 149 | # Uses prepare_operation method 150 | cursor = connection.cursor() 151 | 152 | sql = 'select test from PYHDB_TEST_1 where test=%s' 153 | # correct way: 154 | assert cursor.execute(sql, ['row2']).fetchall() == [('row2',)] 155 | # invalid - extra unexpected parameter 156 | with pytest.raises(ProgrammingError): 157 | cursor.execute(sql, ['row2', 'extra']).fetchall() 158 | 159 | 160 | @pytest.mark.hanatest 161 | def test_cursor_execute_with_params5(connection, test_table_1, content_table_1): 162 | """Test pyformat (named) parameter expansion style""" 163 | # Note: use fetchall() to check that only one row gets returned 164 | cursor = connection.cursor() 165 | 166 | sql = 'select test from {} where test=%(test)s'.format(TABLE) 167 | # correct way: 168 | assert cursor.execute(sql, {'test': 'row2'}).fetchall() == [('row2',)] 169 | # also correct way, additional dict value should just be ignored 170 | assert cursor.execute(sql, {'test': 'row2', 'd': 2}).fetchall() == \ 171 | [('row2',)] 172 | 173 | 174 | @pytest.mark.hanatest 175 | def test_cursor_insert_commit(connection, test_table_1): 176 | cursor = connection.cursor() 177 | cursor.execute("SELECT COUNT(*) FROM %s" % TABLE) 178 | assert cursor.fetchone() == (0,) 179 | 180 | cursor.execute("INSERT INTO %s VALUES('Hello World')" % TABLE) 181 | assert cursor.rowcount == 1 182 | 183 | cursor.execute("SELECT COUNT(*) FROM %s" % TABLE) 184 | assert cursor.fetchone() == (1,) 185 | connection.commit() 186 | 187 | 188 | @pytest.mark.hanatest 189 | def test_cursor_create_and_drop_table(connection): 190 | cursor = connection.cursor() 191 | 192 | if tests.helper.exists_table(connection, TABLE): 193 | cursor.execute('DROP TABLE "%s"' % TABLE) 194 | 195 | assert not tests.helper.exists_table(connection, TABLE) 196 | cursor.execute('CREATE TABLE "%s" ("TEST" VARCHAR(255))' % TABLE) 197 | assert tests.helper.exists_table(connection, TABLE) 198 | 199 | cursor.execute('DROP TABLE "%s"' % TABLE) 200 | 201 | 202 | @pytest.mark.hanatest 203 | def test_received_last_resultset_part_resets_after_execute(connection): 204 | # The private attribute was not reseted to False after 205 | # executing another statement 206 | cursor = connection.cursor() 207 | 208 | cursor.execute("SELECT 1 FROM DUMMY") 209 | # Result is very small we got everything direct into buffer 210 | assert cursor._received_last_resultset_part 211 | 212 | cursor.execute("SELECT VIEW_NAME FROM PUBLIC.VIEWS") 213 | # Result is not small enouth for single resultset part 214 | assert not cursor._received_last_resultset_part 215 | 216 | 217 | @pytest.mark.hanatest 218 | @pytest.mark.parametrize("method", [ 219 | 'fetchone', 220 | 'fetchall', 221 | 'fetchmany', 222 | ]) 223 | def test_fetch_raises_error_after_close(connection, method): 224 | cursor = connection.cursor() 225 | cursor.close() 226 | 227 | with pytest.raises(ProgrammingError): 228 | getattr(cursor, method)() 229 | 230 | 231 | @pytest.mark.hanatest 232 | def test_execute_raises_error_after_close(connection): 233 | cursor = connection.cursor() 234 | cursor.close() 235 | 236 | with pytest.raises(ProgrammingError): 237 | cursor.execute("SELECT TEST FROM DUMMY") 238 | 239 | 240 | @pytest.mark.hanatest 241 | def test_cursor_description_after_execution(connection): 242 | cursor = connection.cursor() 243 | assert cursor.description is None 244 | 245 | cursor.execute("SELECT 'Hello World' AS TEST FROM DUMMY") 246 | assert cursor.description == ((u'TEST', 9, None, 11, 0, None, 0),) 247 | 248 | 249 | @pytest.mark.hanatest 250 | def test_cursor_executemany_python_expansion(connection, test_table_1): 251 | cursor = connection.cursor() 252 | 253 | cursor.executemany( 254 | "INSERT INTO {} VALUES(%s)".format(TABLE), 255 | ( 256 | ("Statement 1",), 257 | ("Statement 2",) 258 | ) 259 | ) 260 | 261 | cursor.execute("SELECT * FROM %s" % TABLE) 262 | result = cursor.fetchall() 263 | assert result == [('Statement 1',), ('Statement 2',)] 264 | 265 | 266 | @pytest.mark.hanatest 267 | def test_cursor_executemany_hana_expansion(connection, test_table_1): 268 | cursor = connection.cursor() 269 | 270 | cursor.executemany( 271 | "INSERT INTO %s VALUES(:1)" % TABLE, 272 | ( 273 | ("Statement 1",), 274 | ("Statement 2",) 275 | ) 276 | ) 277 | 278 | cursor.execute("SELECT * FROM %s" % TABLE) 279 | result = cursor.fetchall() 280 | assert result == [('Statement 1',), ('Statement 2',)] 281 | 282 | @pytest.mark.hanatest 283 | def test_IntegrityError_on_unique_constraint_violation(connection, test_table_1): 284 | cursor = connection.cursor() 285 | cursor.execute("ALTER TABLE %s ADD CONSTRAINT prim_key PRIMARY KEY (TEST)" % TABLE) 286 | 287 | cursor.execute("INSERT INTO %s VALUES('Value 1')" % TABLE) 288 | with pytest.raises(IntegrityError): 289 | cursor.execute("INSERT INTO %s VALUES('Value 1')" % TABLE) 290 | 291 | @pytest.mark.hanatest 292 | def test_prepared_decimal(connection, test_table_2): 293 | cursor = connection.cursor() 294 | cursor.execute("INSERT INTO PYHDB_TEST_2(TEST) VALUES(?)", [Decimal("3.14159265359")]) 295 | 296 | cursor.execute("SELECT * FROM PYHDB_TEST_2") 297 | result = cursor.fetchall() 298 | assert result == [(Decimal("3.14159265359"),)] 299 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /pyhdb/protocol/lobs.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014, 2015 SAP SE. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http: //www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, 10 | # software distributed under the License is distributed on an 11 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | # either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | import io 16 | import logging 17 | from pyhdb.protocol.headers import ReadLobHeader 18 | from pyhdb.protocol.message import RequestMessage 19 | from pyhdb.protocol.segments import RequestSegment 20 | from pyhdb.protocol.constants import message_types, type_codes 21 | from pyhdb.protocol.parts import ReadLobRequest 22 | from pyhdb.compat import PY2, PY3, byte_type 23 | 24 | if PY2: 25 | # Depending on the Python version we use different underlying containers for CLOB strings 26 | import StringIO 27 | import cStringIO 28 | CLOB_STRING_IO_CLASSES = (StringIO.StringIO, cStringIO.InputType, cStringIO.OutputType) 29 | 30 | def CLOB_STRING_IO(init_value): 31 | # factory function to obtain a read-writable StringIO object 32 | # (not possible when directly instantiated with initial value ...) 33 | c = cStringIO.StringIO() 34 | c.write(init_value) 35 | c.seek(0) 36 | return c 37 | else: 38 | CLOB_STRING_IO_CLASSES = (io.StringIO, ) 39 | CLOB_STRING_IO = io.StringIO 40 | 41 | 42 | logger = logging.getLogger('pyhdb') 43 | 44 | SEEK_SET = io.SEEK_SET 45 | SEEK_CUR = io.SEEK_CUR 46 | SEEK_END = io.SEEK_END 47 | 48 | 49 | def from_payload(type_code, payload, connection): 50 | """Generator function to create lob from payload. 51 | Depending on lob type a BLOB, CLOB, or NCLOB instance will be returned. 52 | This function is usually called from types.*LobType.from_resultset() 53 | """ 54 | lob_header = ReadLobHeader(payload) 55 | if lob_header.isnull(): 56 | lob = None 57 | else: 58 | data = payload.read(lob_header.chunk_length) 59 | _LobClass = LOB_TYPE_CODE_MAP[type_code] 60 | lob = _LobClass.from_payload(data, lob_header, connection) 61 | logger.debug('Lob Header %r' % lob) 62 | return lob 63 | 64 | 65 | class Lob(object): 66 | """Base class for all LOB classes""" 67 | 68 | EXTRA_NUM_ITEMS_TO_READ_AFTER_SEEK = 1024 69 | type_code = None 70 | encoding = None 71 | 72 | @classmethod 73 | def from_payload(cls, payload_data, lob_header, connection): 74 | enc_payload_data = cls._decode_lob_data(payload_data) 75 | return cls(enc_payload_data, lob_header, connection) 76 | 77 | @classmethod 78 | def _decode_lob_data(cls, payload_data): 79 | return payload_data.decode(cls.encoding) if cls.encoding else payload_data 80 | 81 | def __init__(self, init_value='', lob_header=None, connection=None): 82 | self.data = self._init_io_container(init_value) 83 | self.data.seek(0) 84 | self._lob_header = lob_header 85 | self._connection = connection 86 | self._current_lob_length = len(self.data.getvalue()) 87 | 88 | @property 89 | def length(self): 90 | """Return total length of a lob. 91 | If a lob was received from the database the length denotes the final absolute length of the lob even if 92 | not all data has yet been read from the database. 93 | For a lob constructed from local data length represents the amount of data currently stored in it. 94 | """ 95 | if self._lob_header: 96 | return self._lob_header.total_lob_length 97 | else: 98 | return self._current_lob_length 99 | 100 | def __len__(self): 101 | return self.length 102 | 103 | def _init_io_container(self, init_value): 104 | raise NotImplemented() 105 | 106 | def tell(self): 107 | """Return position of pointer in lob buffer""" 108 | return self.data.tell() 109 | 110 | def seek(self, offset, whence=SEEK_SET): 111 | """Seek pointer in lob data buffer to requested position. 112 | Might trigger further loading of data from the database if the pointer is beyond currently read data. 113 | """ 114 | # A nice trick is to (ab)use BytesIO.seek() to go to the desired position for easier calculation. 115 | # This will not add any data to the buffer however - very convenient! 116 | self.data.seek(offset, whence) 117 | new_pos = self.data.tell() 118 | missing_bytes_to_read = new_pos - self._current_lob_length 119 | if missing_bytes_to_read > 0: 120 | # Trying to seek beyond currently available LOB data, so need to load some more first. 121 | 122 | # We are smart here: (at least trying...): 123 | # If a user sets a certain file position s/he probably wants to read data from 124 | # there. So already read some extra data to avoid yet another immediate 125 | # reading step. Try with EXTRA_NUM_ITEMS_TO_READ_AFTER_SEEK additional items (bytes/chars). 126 | 127 | # jump to the end of the current buffer and read the new data: 128 | self.data.seek(0, SEEK_END) 129 | self.read(missing_bytes_to_read + self.EXTRA_NUM_ITEMS_TO_READ_AFTER_SEEK) 130 | # reposition file pointer a originally desired position: 131 | self.data.seek(new_pos) 132 | return new_pos 133 | 134 | def read(self, n=-1): 135 | """Read up to n items (bytes/chars) from the lob and return them. 136 | If n is -1 then all available data is returned. 137 | Might trigger further loading of data from the database if the number of items requested for 138 | reading is larger than what is currently buffered. 139 | """ 140 | pos = self.tell() 141 | num_items_to_read = n if n != -1 else self.length - pos 142 | # calculate the position of the file pointer after data was read: 143 | new_pos = min(pos + num_items_to_read, self.length) 144 | 145 | if new_pos > self._current_lob_length: 146 | missing_num_items_to_read = new_pos - self._current_lob_length 147 | self._read_missing_lob_data_from_db(self._current_lob_length, missing_num_items_to_read) 148 | # reposition file pointer to original position as reading in IO buffer might have changed it 149 | self.seek(pos, SEEK_SET) 150 | return self.data.read(n) 151 | 152 | def _read_missing_lob_data_from_db(self, readoffset, readlength): 153 | """Read LOB request part from database""" 154 | logger.debug('Reading missing lob data from db. Offset: %d, readlength: %d' % (readoffset, readlength)) 155 | lob_data = self._make_read_lob_request(readoffset, readlength) 156 | 157 | # make sure we really got as many items (not bytes!) as requested: 158 | enc_lob_data = self._decode_lob_data(lob_data) 159 | assert readlength == len(enc_lob_data), 'expected: %d, received; %d' % (readlength, len(enc_lob_data)) 160 | 161 | # jump to end of data, and append new and properly decoded data to it: 162 | # import pdb;pdb.set_trace() 163 | self.data.seek(0, SEEK_END) 164 | self.data.write(enc_lob_data) 165 | self._current_lob_length = len(self.data.getvalue()) 166 | 167 | def _make_read_lob_request(self, readoffset, readlength): 168 | """Make low level request to HANA database (READLOBREQUEST). 169 | Compose request message with proper parameters and read lob data from second part object of reply. 170 | """ 171 | self._connection._check_closed() 172 | 173 | request = RequestMessage.new( 174 | self._connection, 175 | RequestSegment( 176 | message_types.READLOB, 177 | (ReadLobRequest(self._lob_header.locator_id, readoffset, readlength),) 178 | ) 179 | ) 180 | response = self._connection.send_request(request) 181 | 182 | # The segment of the message contains two parts. 183 | # 1) StatementContext -> ignored for now 184 | # 2) ReadLobReply -> contains some header information and actual LOB data 185 | data_part = response.segments[0].parts[1] 186 | # return actual lob container (BytesIO/TextIO): 187 | return data_part.data 188 | 189 | def getvalue(self): 190 | """Return all currently available lob data (might be shorter than the one in the database)""" 191 | return self.data.getvalue() 192 | 193 | def __str__(self): 194 | """Return string format - might fail for unicode data not representable as string""" 195 | return self.data.getvalue() 196 | 197 | def __repr__(self): 198 | if self._lob_header: 199 | return '<%s length: %d (currently loaded from hana: %d)>' % \ 200 | (self.__class__.__name__, len(self), self._current_lob_length) 201 | else: 202 | return '<%s length: %d>' % (self.__class__.__name__, len(self)) 203 | 204 | def encode(self): 205 | """Encode lob data into binary format""" 206 | raise NotImplemented() 207 | 208 | 209 | class Blob(Lob): 210 | """Instance of this class will be returned for a BLOB object in a db result""" 211 | type_code = type_codes.BLOB 212 | 213 | def _init_io_container(self, init_value): 214 | if isinstance(init_value, io.BytesIO): 215 | return init_value 216 | return io.BytesIO(init_value) 217 | 218 | def encode(self): 219 | return self.getvalue() 220 | 221 | 222 | class _CharLob(Lob): 223 | encoding = None 224 | 225 | def encode(self): 226 | return self.getvalue().encode(self.encoding) 227 | 228 | 229 | class Clob(_CharLob): 230 | """Instance of this class will be returned for a CLOB object in a db result""" 231 | type_code = type_codes.CLOB 232 | encoding = 'ascii' 233 | 234 | def __unicode__(self): 235 | """Convert lob into its unicode format""" 236 | return self.data.getvalue().decode(self.encoding) 237 | 238 | def _init_io_container(self, init_value): 239 | """Initialize container to hold lob data. 240 | Here either a cStringIO or a io.StringIO class is used depending on the Python version. 241 | For CLobs ensure that an initial unicode value only contains valid ascii chars. 242 | """ 243 | if isinstance(init_value, CLOB_STRING_IO_CLASSES): 244 | # already a valid StringIO instance, just use it as it is 245 | v = init_value 246 | else: 247 | # works for strings and unicodes. However unicodes must only contain valid ascii chars! 248 | if PY3: 249 | # a io.StringIO also accepts any unicode characters, but we must be sure that only 250 | # ascii chars are contained. In PY2 we use a cStringIO class which complains by itself 251 | # if it catches this case, so in PY2 no extra check needs to be performed here. 252 | init_value.encode('ascii') # this is just a check, result not needed! 253 | v = CLOB_STRING_IO(init_value) 254 | return v 255 | 256 | 257 | class NClob(_CharLob): 258 | """Instance of this class will be returned for a NCLOB object in a db result""" 259 | type_code = type_codes.NCLOB 260 | encoding = 'utf8' 261 | 262 | def __unicode__(self): 263 | """Convert lob into its unicode format""" 264 | return self.data.getvalue() 265 | 266 | def _init_io_container(self, init_value): 267 | if isinstance(init_value, io.StringIO): 268 | return init_value 269 | 270 | if PY2 and isinstance(init_value, str): 271 | # io.String() only accepts unicode values, so do necessary conversion here: 272 | init_value = init_value.decode(self.encoding) 273 | if PY3 and isinstance(init_value, byte_type): 274 | init_value = init_value.decode(self.encoding) 275 | 276 | return io.StringIO(init_value) 277 | 278 | 279 | LOB_TYPE_CODE_MAP = { 280 | type_codes.BLOB: Blob, 281 | type_codes.CLOB: Clob, 282 | type_codes.NCLOB: NClob, 283 | } 284 | -------------------------------------------------------------------------------- /tests/types/test_geometry.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | import random 3 | # 4 | import pytest 5 | from pyhdb.protocol import types 6 | 7 | # ########################## Test value unpacking ##################################### 8 | 9 | @pytest.mark.parametrize("given,expected", [ 10 | (b"\xFF", None), 11 | (b"\x2d\x50\x4f\x49\x4e\x54\x20\x28\x31\x2e\x30\x30\x30\x30\x30\x30\x30" + \ 12 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x20\x32\x2e\x30\x30\x30\x30\x30" + \ 13 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x29", 14 | "POINT (1.0000000000000000 2.0000000000000000)"), 15 | (b"\x59\x4c\x49\x4e\x45\x53\x54\x52\x49\x4e\x47\x20\x28\x31\x2e\x30\x30" + \ 16 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x20\x32\x2e" + \ 17 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x2c" + \ 18 | b"\x20\x32\x2e\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + \ 19 | b"\x30\x30\x20\x31\x2e\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + \ 20 | b"\x30\x30\x30\x30\x29", 21 | "LINESTRING (1.0000000000000000 2.0000000000000000, " + \ 22 | "2.0000000000000000 1.0000000000000000)"), 23 | (b"\xa7\x50\x4f\x4c\x59\x47\x4f\x4e\x20\x28\x28\x31\x2e\x30\x30\x30\x30" + \ 24 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x20\x31\x2e\x30\x30" + \ 25 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x2c\x20\x30" + \ 26 | b"\x2e\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + \ 27 | b"\x20\x30\x2e\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + \ 28 | b"\x30\x30\x2c\x20\x2d\x31\x2e\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + \ 29 | b"\x30\x30\x30\x30\x30\x30\x20\x31\x2e\x30\x30\x30\x30\x30\x30\x30\x30" + \ 30 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x2c\x20\x31\x2e\x30\x30\x30\x30\x30" + \ 31 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x20\x31\x2e\x30\x30\x30" + \ 32 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x29\x29", 33 | "POLYGON ((1.0000000000000000 1.0000000000000000, " + \ 34 | "0.0000000000000000 0.0000000000000000, " + \ 35 | "-1.0000000000000000 1.0000000000000000, " + \ 36 | "1.0000000000000000 1.0000000000000000))"), 37 | (b"\x32\x4d\x55\x4c\x54\x49\x50\x4f\x49\x4e\x54\x20\x28\x31\x2e\x30\x30" + \ 38 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x20\x32\x2e" + \ 39 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x29", 40 | "MULTIPOINT (1.0000000000000000 2.0000000000000000)"), 41 | (b"\x60\x4d\x55\x4c\x54\x49\x4c\x49\x4e\x45\x53\x54\x52\x49\x4e\x47\x20" + \ 42 | b"\x28\x28\x31\x2e\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + \ 43 | b"\x30\x30\x30\x20\x32\x2e\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + \ 44 | b"\x30\x30\x30\x30\x30\x2c\x20\x32\x2e\x30\x30\x30\x30\x30\x30\x30\x30" + \ 45 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x20\x31\x2e\x30\x30\x30\x30\x30\x30" + \ 46 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x29\x29", 47 | "MULTILINESTRING ((1.0000000000000000 2.0000000000000000, " + \ 48 | "2.0000000000000000 1.0000000000000000))"), 49 | (b"\xae\x4d\x55\x4c\x54\x49\x50\x4f\x4c\x59\x47\x4f\x4e\x20\x28\x28\x28" + \ 50 | b"\x31\x2e\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + \ 51 | b"\x30\x20\x31\x2e\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + \ 52 | b"\x30\x30\x30\x2c\x20\x30\x2e\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + \ 53 | b"\x30\x30\x30\x30\x30\x30\x20\x30\x2e\x30\x30\x30\x30\x30\x30\x30\x30" + \ 54 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x2c\x20\x2d\x31\x2e\x30\x30\x30\x30" + \ 55 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x20\x31\x2e\x30\x30" + \ 56 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x2c\x20\x31" + \ 57 | b"\x2e\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + \ 58 | b"\x20\x31\x2e\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + \ 59 | b"\x30\x30\x29\x29\x29", 60 | "MULTIPOLYGON (((1.0000000000000000 1.0000000000000000, " + \ 61 | "0.0000000000000000 0.0000000000000000, " + \ 62 | "-1.0000000000000000 1.0000000000000000, " + \ 63 | "1.0000000000000000 1.0000000000000000)))"), 64 | ]) 65 | def test_unpack_geometry_wkt(given, expected): 66 | given = BytesIO(given) 67 | assert types.Geometry.from_resultset(given) == expected 68 | 69 | 70 | # ########################## Test value packing ##################################### 71 | 72 | @pytest.mark.parametrize("given,expected", [ 73 | (None, b"\x1d\xFF", ), 74 | ("POINT (1.0000000000000000 2.0000000000000000)", 75 | b"\x1d\x2d\x50\x4f\x49\x4e\x54\x20\x28\x31\x2e\x30\x30\x30\x30\x30\x30" + \ 76 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x20\x32\x2e\x30\x30\x30\x30" + \ 77 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x29"), 78 | ("LINESTRING (1.0000000000000000 2.0000000000000000, " + \ 79 | "2.0000000000000000 1.0000000000000000)", 80 | b"\x1d\x59\x4c\x49\x4e\x45\x53\x54\x52\x49\x4e\x47\x20\x28\x31\x2e\x30" + \ 81 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x20\x32" + \ 82 | b"\x2e\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + \ 83 | b"\x2c\x20\x32\x2e\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + \ 84 | b"\x30\x30\x30\x20\x31\x2e\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + \ 85 | b"\x30\x30\x30\x30\x30\x29"), 86 | ("POLYGON ((1.0000000000000000 1.0000000000000000, " + \ 87 | "0.0000000000000000 0.0000000000000000, " + \ 88 | "-1.0000000000000000 1.0000000000000000, " + \ 89 | "1.0000000000000000 1.0000000000000000))", 90 | b"\x1d\xa7\x50\x4f\x4c\x59\x47\x4f\x4e\x20\x28\x28\x31\x2e\x30\x30\x30" + \ 91 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x20\x31\x2e\x30" + \ 92 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x2c\x20" + \ 93 | b"\x30\x2e\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + \ 94 | b"\x30\x20\x30\x2e\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + \ 95 | b"\x30\x30\x30\x2c\x20\x2d\x31\x2e\x30\x30\x30\x30\x30\x30\x30\x30\x30" + \ 96 | b"\x30\x30\x30\x30\x30\x30\x30\x20\x31\x2e\x30\x30\x30\x30\x30\x30\x30" + \ 97 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x2c\x20\x31\x2e\x30\x30\x30\x30" + \ 98 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x20\x31\x2e\x30\x30" + \ 99 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x29\x29"), 100 | ("MULTIPOINT (1.0000000000000000 2.0000000000000000)", 101 | b"\x1d\x32\x4d\x55\x4c\x54\x49\x50\x4f\x49\x4e\x54\x20\x28\x31\x2e\x30" + \ 102 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x20\x32" + \ 103 | b"\x2e\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x29"), 104 | ("MULTILINESTRING ((1.0000000000000000 2.0000000000000000, " + \ 105 | "2.0000000000000000 1.0000000000000000))", 106 | b"\x1d\x60\x4d\x55\x4c\x54\x49\x4c\x49\x4e\x45\x53\x54\x52\x49\x4e\x47" + \ 107 | b"\x20\x28\x28\x31\x2e\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + \ 108 | b"\x30\x30\x30\x30\x20\x32\x2e\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + \ 109 | b"\x30\x30\x30\x30\x30\x30\x2c\x20\x32\x2e\x30\x30\x30\x30\x30\x30\x30" + \ 110 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x20\x31\x2e\x30\x30\x30\x30\x30" + \ 111 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x29\x29"), 112 | ("MULTIPOLYGON (((1.0000000000000000 1.0000000000000000, " + \ 113 | "0.0000000000000000 0.0000000000000000, " + \ 114 | "-1.0000000000000000 1.0000000000000000, " + \ 115 | "1.0000000000000000 1.0000000000000000)))", 116 | b"\x1d\xae\x4d\x55\x4c\x54\x49\x50\x4f\x4c\x59\x47\x4f\x4e\x20\x28\x28" + \ 117 | b"\x28\x31\x2e\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + \ 118 | b"\x30\x30\x20\x31\x2e\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + \ 119 | b"\x30\x30\x30\x30\x2c\x20\x30\x2e\x30\x30\x30\x30\x30\x30\x30\x30\x30" + \ 120 | b"\x30\x30\x30\x30\x30\x30\x30\x20\x30\x2e\x30\x30\x30\x30\x30\x30\x30" + \ 121 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x2c\x20\x2d\x31\x2e\x30\x30\x30" + \ 122 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x20\x31\x2e\x30" + \ 123 | b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x2c\x20" + \ 124 | b"\x31\x2e\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + \ 125 | b"\x30\x20\x31\x2e\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + \ 126 | b"\x30\x30\x30\x29\x29\x29"), 127 | ]) 128 | def test_pack_geometry_wkt(given, expected): 129 | assert types.Geometry.prepare(given) == expected 130 | 131 | 132 | # ############################################################################################################# 133 | # Real HANA interaction with geormetry (integration tests) 134 | # ############################################################################################################# 135 | 136 | import tests.helper 137 | TABLE = 'PYHDB_TEST_GEOMETRY' 138 | TABLE_POINT = TABLE + "_POINT" 139 | TABLE_GEOMETRY = TABLE + "_GEOMETRY" 140 | TABLE_FIELDS_POINT = "point ST_POINT NOT NULL" 141 | TABLE_FIELDS_GEOMETRY = "geo ST_GEOMETRY NOT NULL" 142 | 143 | @pytest.fixture 144 | def test_table_point(request, connection): 145 | tests.helper.create_table_fixture(request, connection, TABLE_POINT, 146 | TABLE_FIELDS_POINT, column_table=True) 147 | 148 | @pytest.fixture 149 | def test_table_geometry(request, connection): 150 | tests.helper.create_table_fixture(request, connection, TABLE_GEOMETRY, 151 | TABLE_FIELDS_GEOMETRY, column_table=True) 152 | 153 | 154 | @pytest.mark.hanatest 155 | def test_insert_point(connection, test_table_point): 156 | """Insert spatial point into table""" 157 | cursor = connection.cursor() 158 | point_x = random.randint(-100.0, 100.0) 159 | point_y = random.randint(-100.0, 100.0) 160 | wkt_string = "POINT(%f %f)" % (point_x, point_y) 161 | cursor.execute("insert into %s (point) values (:1)" % TABLE_POINT, [wkt_string]) 162 | connection.commit() 163 | cursor = connection.cursor() 164 | row = cursor.execute('select point.ST_X(), point.ST_Y() from %s' % TABLE_POINT).fetchone() 165 | assert row[0] == point_x 166 | assert row[1] == point_y 167 | 168 | 169 | @pytest.mark.hanatest 170 | def test_insert_linestring(connection, test_table_geometry): 171 | """Insert spatial linestring into table""" 172 | cursor = connection.cursor() 173 | point1_x = random.randint(-100.0, 100.0) 174 | point1_y = random.randint(-100.0, 100.0) 175 | point2_x = random.randint(-100.0, 100.0) 176 | point2_y = random.randint(-100.0, 100.0) 177 | wkt_string = "LINESTRING(%f %f, %f %f)" % (point1_x, point1_y, point2_x, point2_y) 178 | cursor.execute("insert into %s (geo) values (:1)" % TABLE_GEOMETRY, [wkt_string]) 179 | connection.commit() 180 | cursor = connection.cursor() 181 | sql = """ 182 | Select geo.ST_StartPoint().ST_X(), geo.ST_StartPoint().ST_Y(), 183 | geo.ST_EndPoint().ST_X(), geo.ST_EndPoint().ST_Y() 184 | From %s 185 | """ 186 | row = cursor.execute(sql % TABLE_GEOMETRY).fetchone() 187 | assert row[0] == point1_x 188 | assert row[1] == point1_y 189 | assert row[2] == point2_x 190 | assert row[3] == point2_y 191 | 192 | 193 | @pytest.mark.hanatest 194 | def test_insert_polygon(connection, test_table_geometry): 195 | """Insert spatial polygon into table""" 196 | cursor = connection.cursor() 197 | # The edges of a polygon can not cross. Therefore we build an arbitrary quadtrangle. 198 | point1_x = random.randint(0, 100.0) 199 | point1_y = random.randint(0, 100.0) 200 | point2_x = random.randint(0, 100.0) 201 | point2_y = random.randint(-100.0, 0) 202 | point3_x = random.randint(-100.0, 0) 203 | point3_y = random.randint(-100.0, 0) 204 | point4_x = random.randint(-100.0, 0) 205 | point4_y = random.randint(0, 100.0) 206 | wkt_string = "POLYGON((%f %f, %f %f, %f %f, %f %f, %f %f))" % ( 207 | point1_x, point1_y, point2_x, point2_y, point3_x, point3_y, 208 | point4_x, point4_y, point1_x, point1_y 209 | ) 210 | cursor.execute("insert into %s (geo) values (:1)" % TABLE_GEOMETRY, [wkt_string]) 211 | connection.commit() 212 | cursor = connection.cursor() 213 | # We don't want to check all points of the polygon. 214 | # We will only check the minimal and maximal values. 215 | sql = """ 216 | Select geo.ST_XMin(), geo.ST_XMax(), geo.ST_YMin(), geo.ST_YMax() 217 | From %s 218 | """ 219 | row = cursor.execute(sql % TABLE_GEOMETRY).fetchone() 220 | assert row[0] == min(point1_x, point2_x, point3_x, point4_x) 221 | assert row[1] == max(point1_x, point2_x, point3_x, point4_x) 222 | assert row[2] == min(point1_y, point2_y, point3_y, point4_y) 223 | assert row[3] == max(point1_y, point2_y, point3_y, point4_y) 224 | --------------------------------------------------------------------------------