From 74e778530976e80519c59d54aefd602a4f799c1c Mon Sep 17 00:00:00 2001 From: Stephen Darlington Date: Wed, 26 May 2021 14:08:22 +0100 Subject: [PATCH 1/5] IGNITE-12867 Inital attempt at DBAPI support --- pyignite/__init__.py | 3 + pyignite/dbapi/__init__.py | 76 ++++++++++++++++++ pyignite/dbapi/dbclient.py | 47 +++++++++++ pyignite/dbapi/dbcursor.py | 154 +++++++++++++++++++++++++++++++++++++ pyignite/dbapi/errors.py | 54 +++++++++++++ 5 files changed, 334 insertions(+) create mode 100644 pyignite/dbapi/__init__.py create mode 100644 pyignite/dbapi/dbclient.py create mode 100644 pyignite/dbapi/dbcursor.py create mode 100644 pyignite/dbapi/errors.py diff --git a/pyignite/__init__.py b/pyignite/__init__.py index 1b0a9c2..3508976 100644 --- a/pyignite/__init__.py +++ b/pyignite/__init__.py @@ -16,5 +16,8 @@ from pyignite.client import Client from pyignite.aio_client import AioClient from pyignite.binary import GenericObjectMeta +from .dbapi import connect __version__ = '0.6.0-dev' + +__all__ = [ 'Client', 'connect' ] diff --git a/pyignite/dbapi/__init__.py b/pyignite/dbapi/__init__.py new file mode 100644 index 0000000..6b03f3a --- /dev/null +++ b/pyignite/dbapi/__init__.py @@ -0,0 +1,76 @@ +# +# Copyright 2021 GridGain Systems, Inc. and Contributors. +# +# Licensed under the GridGain Community Edition License (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.gridgain.com/products/software/community-edition/gridgain-community-edition-license +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from .dbclient import DBClient +from .. import constants + +apiLevel = '2.0' +threadsafety = 2 +paramstyle = 'qmark' + +def connect(dsn=None, + user=None, password=None, + host=constants.IGNITE_DEFAULT_HOST, port=constants.IGNITE_DEFAULT_PORT, + **kwargs): + """ + Create a new database connection. + + The connection can be specified via DSN: + + ``conn = connect("ignite://localhost/test?param1=value1&...")`` + + or using database and credentials arguments: + + ``conn = connect(database="test", user="default", password="default", + host="localhost", **kwargs)`` + + The basic connection parameters are: + + - *host*: host with running Ignite server. + - *port*: port Ignite server is bound to. + - *database*: database connect to. + - *user*: database user. + - *password*: user's password. + + See defaults in :data:`~pygridgain.connection.Connection` + constructor. + + DSN or host is required. + + Any other keyword parameter will be passed to the underlying Connection + class. + + :return: a new connection. + """ + + if dsn is None and host is None: + raise ValueError('host or dsn is required') + + # TODO: implement connection using DSN + if host is None: + raise ValueError('dsn connection is not currently implemented') + + client = DBClient() + client.connect(host, port) + + return client + +__all__ = [ + 'connect', + 'Warning', 'Error', 'DataError', 'DatabaseError', 'ProgrammingError', + 'IntegrityError', 'InterfaceError', 'InternalError', 'NotSupportedError', + 'OperationalError' +] diff --git a/pyignite/dbapi/dbclient.py b/pyignite/dbapi/dbclient.py new file mode 100644 index 0000000..3497612 --- /dev/null +++ b/pyignite/dbapi/dbclient.py @@ -0,0 +1,47 @@ +# +# Copyright 2021 GridGain Systems, Inc. and Contributors. +# +# Licensed under the GridGain Community Edition License (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.gridgain.com/products/software/community-edition/gridgain-community-edition-license +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from ..client import Client +from .dbcursor import DBCursor + +class DBClient (Client): + + + def close(self): + """ + """ + # TODO: close ope cursors + super.close() + + def commit(self): + """ + Ignite doesn't have SQL transactions + """ + pass + + def rollback(self): + """ + Ignite doesn't have SQL transactions + """ + pass + + def cursor(self): + """ + Cursors work slightly differently in Ignite versus DBAPI, so + we map from one to the other + """ + return DBCursor(self) + \ No newline at end of file diff --git a/pyignite/dbapi/dbcursor.py b/pyignite/dbapi/dbcursor.py new file mode 100644 index 0000000..06114c7 --- /dev/null +++ b/pyignite/dbapi/dbcursor.py @@ -0,0 +1,154 @@ +# +# Copyright 2021 GridGain Systems, Inc. and Contributors. +# +# Licensed under the GridGain Community Edition License (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.gridgain.com/products/software/community-edition/gridgain-community-edition-license +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from ..cursors import SqlCursor + +class DBCursor: + + + def __init__(self, connection): + self.connection = connection + self.cursor = None + + @property + def description(self): + if self._state == self._states.NONE: + return None + + columns = self._columns or [] + types = self._types or [] + + return [ + Column(name, type_code, None, None, None, None, True) + for name, type_code in zip(columns, types) + ] + + @property + def rowcount(self): + """ + :return: the number of rows that the last .execute*() produced. + """ + return self._rowcount + + def close(self): + """ + Close the cursor now. The cursor will be unusable from this point + forward; an :data:`~clickhouse_driver.dbapi.Error` (or subclass) + exception will be raised if any operation is attempted with the + cursor. + """ + self._client.disconnect() + self._state = self._states.CURSOR_CLOSED + + try: + # cursor can be already closed + self._connection.cursors.remove(self) + except ValueError: + pass + + def execute(self, operation, parameters=None): + """ + Prepare and execute a database operation (query or command). + + :param operation: query or command to execute. + :param parameters: sequence or mapping that will be bound to + variables in the operation. + :return: None + """ + self.cursor = self.connection.sql(operation, query_args=parameters) + + def executemany(self, operation, seq_of_parameters): + """ + Prepare a database operation (query or command) and then execute it + against all parameter sequences found in the sequence + `seq_of_parameters`. + + :param operation: query or command to execute. + :param seq_of_parameters: sequences or mappings for execution. + :return: None + """ + pass + + def fetchone(self): + """ + Fetch the next row of a query result set, returning a single sequence, + or None when no more data is available. + + :return: the next row of a query result set or None. + """ + self._check_query_started() + + if self._stream_results: + return next(self._rows, None) + + else: + if not self._rows: + return None + + return self._rows.pop(0) + + def fetchmany(self, size=None): + """ + Fetch the next set of rows of a query result, returning a sequence of + sequences (e.g. a list of tuples). An empty sequence is returned when + no more rows are available. + + :param size: amount of rows to return. + :return: list of fetched rows or empty list. + """ + self._check_query_started() + + if size is None: + size = self.arraysize + + if self._stream_results: + if size == -1: + return list(self._rows) + else: + return list(islice(self._rows, size)) + + if size < 0: + rv = self._rows + self._rows = [] + else: + rv = self._rows[:size] + self._rows = self._rows[size:] + + return rv + + def fetchall(self): + """ + Fetch all (remaining) rows of a query result, returning them as a + sequence of sequences (e.g. a list of tuples). + + :return: list of fetched rows. + """ + if self.cursor != None: + rows = [] + for row in self.cursor: + rows.append(row) + else: + return None # error? + + return rows + + def setinputsizes(self, sizes): + # Do nothing. + pass + + def setoutputsize(self, size, column=None): + # Do nothing. + pass diff --git a/pyignite/dbapi/errors.py b/pyignite/dbapi/errors.py new file mode 100644 index 0000000..962366e --- /dev/null +++ b/pyignite/dbapi/errors.py @@ -0,0 +1,54 @@ +# +# Copyright 2021 GridGain Systems, Inc. and Contributors. +# +# Licensed under the GridGain Community Edition License (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.gridgain.com/products/software/community-edition/gridgain-community-edition-license +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Warning(Exception): + pass + + +class Error(Exception): + pass + + +class InterfaceError(Error): + pass + + +class DatabaseError(Error): + pass + + +class InternalError(DatabaseError): + pass + + +class OperationalError(DatabaseError): + pass + + +class ProgrammingError(DatabaseError): + pass + + +class IntegrityError(DatabaseError): + pass + + +class DataError(DatabaseError): + pass + + +class NotSupportedError(DatabaseError): + pass From 6717189efa1cf4a500263c801e84f7ddd1ff5ee9 Mon Sep 17 00:00:00 2001 From: Stephen Darlington Date: Thu, 1 Jul 2021 17:09:30 +0100 Subject: [PATCH 2/5] implement DSN parsing --- pyignite/dbapi/__init__.py | 29 +++++++++++++++++++++-------- pyignite/dbapi/dbclient.py | 2 +- pyignite/dbapi/dbcursor.py | 2 +- pyignite/dbapi/errors.py | 9 --------- 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/pyignite/dbapi/__init__.py b/pyignite/dbapi/__init__.py index 6b03f3a..72e45e6 100644 --- a/pyignite/dbapi/__init__.py +++ b/pyignite/dbapi/__init__.py @@ -16,6 +16,7 @@ from .dbclient import DBClient from .. import constants +from urllib.parse import urlparse, parse_qs apiLevel = '2.0' threadsafety = 2 @@ -45,7 +46,7 @@ def connect(dsn=None, - *user*: database user. - *password*: user's password. - See defaults in :data:`~pygridgain.connection.Connection` + See defaults in :data:`~pyignite.connection.Connection` constructor. DSN or host is required. @@ -55,19 +56,31 @@ def connect(dsn=None, :return: a new connection. """ - - if dsn is None and host is None: - raise ValueError('host or dsn is required') - - # TODO: implement connection using DSN - if host is None: - raise ValueError('dsn connection is not currently implemented') + + if dsn is not None: + parsed_dsn = _parse_dsn(dsn) + host = parsed_dsn['host'] + port = parsed_dsn['port'] client = DBClient() client.connect(host, port) return client +def _parse_dsn(dsn): + url_components = urlparse(dsn) + host = url_components.hostname + if url_components.port is not None: + port = url_components.port + else: + port = 10800 + if url_components.path is not None: + schema = url_components.path.replace('/', '') + else: + schema = 'PUBLIC' + schema = url_components.path + return { 'host':host, 'port':port, 'schema':schema } + __all__ = [ 'connect', 'Warning', 'Error', 'DataError', 'DatabaseError', 'ProgrammingError', diff --git a/pyignite/dbapi/dbclient.py b/pyignite/dbapi/dbclient.py index 3497612..220a05e 100644 --- a/pyignite/dbapi/dbclient.py +++ b/pyignite/dbapi/dbclient.py @@ -23,7 +23,7 @@ class DBClient (Client): def close(self): """ """ - # TODO: close ope cursors + # TODO: close open cursors super.close() def commit(self): diff --git a/pyignite/dbapi/dbcursor.py b/pyignite/dbapi/dbcursor.py index 06114c7..a8bd914 100644 --- a/pyignite/dbapi/dbcursor.py +++ b/pyignite/dbapi/dbcursor.py @@ -46,7 +46,7 @@ def rowcount(self): def close(self): """ Close the cursor now. The cursor will be unusable from this point - forward; an :data:`~clickhouse_driver.dbapi.Error` (or subclass) + forward; an :data:`~pyignite.dbapi.Error` (or subclass) exception will be raised if any operation is attempted with the cursor. """ diff --git a/pyignite/dbapi/errors.py b/pyignite/dbapi/errors.py index 962366e..07b315f 100644 --- a/pyignite/dbapi/errors.py +++ b/pyignite/dbapi/errors.py @@ -17,38 +17,29 @@ class Warning(Exception): pass - class Error(Exception): pass - class InterfaceError(Error): pass - class DatabaseError(Error): pass - class InternalError(DatabaseError): pass - class OperationalError(DatabaseError): pass - class ProgrammingError(DatabaseError): pass - class IntegrityError(DatabaseError): pass - class DataError(DatabaseError): pass - class NotSupportedError(DatabaseError): pass From d27a4bf03c93aada2b21ba9cf91b0992d4410ded Mon Sep 17 00:00:00 2001 From: Stephen Darlington Date: Fri, 2 Jul 2021 18:00:09 +0100 Subject: [PATCH 3/5] Beginnings of a SQLAlchemy dialect --- pyignite/dbapi/__init__.py | 10 +- pyignite/dbapi/dbcursor.py | 52 +++---- pyignite/dbapi/sqlalchemy.py | 277 +++++++++++++++++++++++++++++++++++ 3 files changed, 300 insertions(+), 39 deletions(-) create mode 100644 pyignite/dbapi/sqlalchemy.py diff --git a/pyignite/dbapi/__init__.py b/pyignite/dbapi/__init__.py index 72e45e6..495fef5 100644 --- a/pyignite/dbapi/__init__.py +++ b/pyignite/dbapi/__init__.py @@ -15,6 +15,7 @@ # from .dbclient import DBClient +from .errors import * from .. import constants from urllib.parse import urlparse, parse_qs @@ -70,10 +71,7 @@ def connect(dsn=None, def _parse_dsn(dsn): url_components = urlparse(dsn) host = url_components.hostname - if url_components.port is not None: - port = url_components.port - else: - port = 10800 + port = url_components.port or 10800 if url_components.path is not None: schema = url_components.path.replace('/', '') else: @@ -82,8 +80,8 @@ def _parse_dsn(dsn): return { 'host':host, 'port':port, 'schema':schema } __all__ = [ - 'connect', + 'connect', 'apiLevel', 'threadsafety', 'paramstyle', 'Warning', 'Error', 'DataError', 'DatabaseError', 'ProgrammingError', 'IntegrityError', 'InterfaceError', 'InternalError', 'NotSupportedError', - 'OperationalError' + 'OperationalError', 'IgniteDialect' ] diff --git a/pyignite/dbapi/dbcursor.py b/pyignite/dbapi/dbcursor.py index a8bd914..a6009c8 100644 --- a/pyignite/dbapi/dbcursor.py +++ b/pyignite/dbapi/dbcursor.py @@ -16,33 +16,24 @@ from ..cursors import SqlCursor -class DBCursor: - +class DBCursor(object): def __init__(self, connection): self.connection = connection self.cursor = None + self.rowcount = -1 @property def description(self): - if self._state == self._states.NONE: - return None - - columns = self._columns or [] - types = self._types or [] +# columns = self._columns +# types = [ bool ] return [ - Column(name, type_code, None, None, None, None, True) - for name, type_code in zip(columns, types) + [name, None, None, None, None, None, True] + for name in self._columns +# for name, type_code in zip(columns, types) ] - @property - def rowcount(self): - """ - :return: the number of rows that the last .execute*() produced. - """ - return self._rowcount - def close(self): """ Close the cursor now. The cursor will be unusable from this point @@ -50,14 +41,14 @@ def close(self): exception will be raised if any operation is attempted with the cursor. """ - self._client.disconnect() - self._state = self._states.CURSOR_CLOSED +# self.connection.disconnect() +# self._state = self._states.CURSOR_CLOSED - try: - # cursor can be already closed - self._connection.cursors.remove(self) - except ValueError: - pass +# try: +# # cursor can be already closed +# self.connection.cursors.remove(self) +# except ValueError: +# pass def execute(self, operation, parameters=None): """ @@ -68,7 +59,8 @@ def execute(self, operation, parameters=None): variables in the operation. :return: None """ - self.cursor = self.connection.sql(operation, query_args=parameters) + self.cursor = self.connection.sql(operation, query_args=parameters, include_field_names=True) + self._columns = next(self.cursor) def executemany(self, operation, seq_of_parameters): """ @@ -89,16 +81,10 @@ def fetchone(self): :return: the next row of a query result set or None. """ - self._check_query_started() - - if self._stream_results: - return next(self._rows, None) - + if self.cursor is not None: + return next(self.cursor) else: - if not self._rows: - return None - - return self._rows.pop(0) + return None def fetchmany(self, size=None): """ diff --git a/pyignite/dbapi/sqlalchemy.py b/pyignite/dbapi/sqlalchemy.py new file mode 100644 index 0000000..6ded593 --- /dev/null +++ b/pyignite/dbapi/sqlalchemy.py @@ -0,0 +1,277 @@ +# +# Copyright 2021 GridGain Systems, Inc. and Contributors. +# +# Licensed under the GridGain Community Edition License (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.gridgain.com/products/software/community-edition/gridgain-community-edition-license +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from sqlalchemy import types +from sqlalchemy.engine import default +from sqlalchemy.sql import compiler + +from .. import dbapi +# from . dbclient import DBClient +from .. datatypes.type_names import * + +RESERVED_SCHEMAS = ['IGNITE', 'SYS'] + + +# type_map = { +# "char": types.String, +# "varchar": types.String, +# "float": types.Float, +# "decimal": types.Float, +# "real": types.Float, +# "double": types.Float, +# "boolean": types.Boolean, +# "tinyint": types.BigInteger, +# "smallint": types.BigInteger, +# "integer": types.BigInteger, +# "bigint": types.BigInteger, +# "timestamp": types.TIMESTAMP, +# "date": types.DATE, +# "other": types.BLOB, +# } + +type_map = { + NAME_BYTE: types.BigInteger, + NAME_SHORT: types.BigInteger, + NAME_INT: types.BigInteger, + NAME_LONG: types.BigInteger, + NAME_FLOAT: types.Float, + NAME_DOUBLE: types.Float, + NAME_CHAR: types.String, + NAME_BOOLEAN: types.Boolean, + NAME_STRING: types.String, + # NAME_UUID = 'java.util.UUID' + # NAME_DATE = 'java.util.Date' + # NAME_BYTE_ARR = 'class [B' + # NAME_SHORT_ARR = 'class [S' + # NAME_INT_ARR = 'class [I' + # NAME_LONG_ARR = 'class [J' + # NAME_FLOAT_ARR = 'class [F' + # NAME_DOUBLE_ARR = 'class [D' + # NAME_CHAR_ARR = 'class [C' + # NAME_BOOLEAN_ARR = 'class [Z' + # NAME_STRING_ARR = 'class [Ljava.lang.String;' + # NAME_UUID_ARR = 'class [Ljava.util.UUID;' + # NAME_DATE_ARR = 'class [Ljava.util.Date;' + # NAME_OBJ_ARR = 'class [Ljava.lang.Object;' + # NAME_COL = 'java.util.Collection' + # NAME_MAP = 'java.util.Map' + # NAME_DECIMAL = 'java.math.BigDecimal' + # NAME_DECIMAL_ARR = 'class [Ljava.math.BigDecimal;' + # NAME_TIMESTAMP = 'java.sql.Timestamp' + # NAME_TIMESTAMP_ARR = 'class [Ljava.sql.Timestamp;' + # NAME_TIME = 'java.sql.Time' + # NAME_TIME_ARR = 'class [Ljava.sql.Time;' +} + + +class UniversalSet(object): + def __contains__(self, item): + return True + + +class IgniteIdentifierPreparer(compiler.IdentifierPreparer): + reserved_words = UniversalSet() + + +class IgniteCompiler(compiler.SQLCompiler): + pass + + +class IgniteTypeCompiler(compiler.GenericTypeCompiler): + def visit_REAL(self, type_, **kwargs): + return "DOUBLE" + + def visit_NUMERIC(self, type_, **kwargs): + return "LONG" + + visit_DECIMAL = visit_NUMERIC + visit_INTEGER = visit_NUMERIC + visit_SMALLINT = visit_NUMERIC + visit_BIGINT = visit_NUMERIC + visit_BOOLEAN = visit_NUMERIC + visit_TIMESTAMP = visit_NUMERIC + visit_DATE = visit_NUMERIC + + def visit_CHAR(self, type_, **kwargs): + return "VARCHAR" + + visit_NCHAR = visit_CHAR + visit_VARCHAR = visit_CHAR + visit_NVARCHAR = visit_CHAR + visit_TEXT = visit_CHAR + + def visit_DATETIME(self, type_, **kwargs): + return "LONG" + + def visit_TIME(self, type_, **kwargs): + return "LONG" + + def visit_BLOB(self, type_, **kwargs): + return "COMPLEX" + + visit_CLOB = visit_BLOB + visit_NCLOB = visit_BLOB + visit_VARBINARY = visit_BLOB + visit_BINARY = visit_BLOB + + +class IgniteDialect(default.DefaultDialect): + + name = "ignite" + scheme = "thin" +# driver = "rest" + user = None + password = None + preparer = IgniteIdentifierPreparer + statement_compiler = IgniteCompiler + type_compiler = IgniteTypeCompiler + supports_alter = False + supports_views = False + postfetch_lastrowid = False + supports_pk_autoincrement = False + supports_default_values = False + supports_empty_insert = False + supports_unicode_statements = True + supports_unicode_binds = True + returns_unicode_strings = True + description_encoding = None + supports_native_boolean = True + + def __init__(self, context=None, *args, **kwargs): + super(IgniteDialect, self).__init__(*args, **kwargs) + self.context = context or {} + + @classmethod + def dbapi(cls): + return dbapi + + def create_connect_args(self, url): + kwargs = { + "host": url.host, + "port": url.port or 10800, + "user": url.username or None, + "password": url.password or None, + "path": url.database, + "scheme": self.scheme, + "context": self.context, + "header": url.query.get("header") == "true", + } + return ([], kwargs) + + def get_schema_names(self, connection, **kwargs): + # Each Ignite datasource appears as a table in the "SYS" schema. + result = connection.execute( + "SELECT SCHEMA_NAME FROM SYS.SCHEMAS" + ) + + return [ + row.SCHEMA_NAME for row in result if row.SCHEMA_NAME not in RESERVED_SCHEMAS + ] + + def has_table(self, connection, table_name, schema=None): + query = """ + SELECT COUNT(*) > 0 AS exists_ + FROM SYS.TABLES + WHERE TABLE_NAME = '{table_name}' + """.format( + table_name=table_name + ) + + result = connection.execute(query) + return result.fetchone().EXISTS_ + + def get_table_names(self, connection, schema=None, **kwargs): + query = "SELECT TABLE_NAME FROM SYS.TABLES" + if schema: + query = "{query} WHERE TABLE_SCHEMA = '{schema}'".format( + query=query, schema=schema + ) + + result = connection.execute(query) + return [row.TABLE_NAME for row in result] + + def get_view_names(self, connection, schema=None, **kwargs): + return [] + + def get_table_options(self, connection, table_name, schema=None, **kwargs): + return {} + + def get_columns(self, connection, table_name, schema=None, **kwargs): + query = """ + SELECT COLUMN_NAME, + TYPE, + NULLABLE, + DEFAULT_VALUE + FROM SYS.TABLE_COLUMNS + WHERE TABLE_NAME = '{table_name}' + """.format( + table_name=table_name + ) + if schema: + query = "{query} AND SCHEMA_NAME = '{schema}'".format( + query=query, schema=schema + ) + + result = connection.execute(query) + + return [ + { + "name": row.COLUMN_NAME, + "type": type_map[row.DATA_TYPE.lower()], + "nullable": get_is_nullable(row.IS_NULLABLE), + "default": get_default(row.COLUMN_DEFAULT), + } + for row in result + ] + + def get_pk_constraint(self, connection, table_name, schema=None, **kwargs): + return {"constrained_columns": [], "name": None} + + def get_foreign_keys(self, connection, table_name, schema=None, **kwargs): + return [] + + def get_check_constraints(self, connection, table_name, schema=None, **kwargs): + return [] + + def get_table_comment(self, connection, table_name, schema=None, **kwargs): + return {"text": ""} + + def get_indexes(self, connection, table_name, schema=None, **kwargs): + return [] + + def get_unique_constraints(self, connection, table_name, schema=None, **kwargs): + return [] + + def get_view_definition(self, connection, view_name, schema=None, **kwargs): + pass + + def do_rollback(self, dbapi_connection): + pass + + def _check_unicode_returns(self, connection, additional_tests=None): + return True + + def _check_unicode_description(self, connection): + return True + +def get_is_nullable(Ignite_is_nullable): + # this should be 'YES' or 'NO'; we default to no + return Ignite_is_nullable.lower() == "yes" + + +def get_default(Ignite_column_default): + # currently unused, returns '' + return str(Ignite_column_default) if Ignite_column_default != "" else None \ No newline at end of file From cd225a9b3e4b293cb1431c7be0830c35c2774db2 Mon Sep 17 00:00:00 2001 From: Stephen Darlington Date: Fri, 2 Jul 2021 18:27:37 +0100 Subject: [PATCH 4/5] Make CREATE TABLE work when there are FK constraints --- pyignite/dbapi/sqlalchemy.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyignite/dbapi/sqlalchemy.py b/pyignite/dbapi/sqlalchemy.py index 6ded593..2decc90 100644 --- a/pyignite/dbapi/sqlalchemy.py +++ b/pyignite/dbapi/sqlalchemy.py @@ -89,6 +89,9 @@ class IgniteIdentifierPreparer(compiler.IdentifierPreparer): class IgniteCompiler(compiler.SQLCompiler): pass +class IgniteDDLCompiler(compiler.DDLCompiler): + def visit_foreign_key_constraint(self, constraint, **kw): + return None class IgniteTypeCompiler(compiler.GenericTypeCompiler): def visit_REAL(self, type_, **kwargs): @@ -138,6 +141,7 @@ class IgniteDialect(default.DefaultDialect): preparer = IgniteIdentifierPreparer statement_compiler = IgniteCompiler type_compiler = IgniteTypeCompiler + ddl_compiler = IgniteDDLCompiler supports_alter = False supports_views = False postfetch_lastrowid = False From 902315050c7ea708a3ff92f143577bd1803b0b20 Mon Sep 17 00:00:00 2001 From: Stephen Darlington Date: Mon, 5 Jul 2021 09:58:22 +0100 Subject: [PATCH 5/5] Fix copyright messages --- pyignite/dbapi/__init__.py | 15 +++++++-------- pyignite/dbapi/dbclient.py | 15 +++++++-------- pyignite/dbapi/dbcursor.py | 15 +++++++-------- pyignite/dbapi/errors.py | 15 +++++++-------- pyignite/dbapi/sqlalchemy.py | 15 +++++++-------- 5 files changed, 35 insertions(+), 40 deletions(-) diff --git a/pyignite/dbapi/__init__.py b/pyignite/dbapi/__init__.py index 495fef5..aaee234 100644 --- a/pyignite/dbapi/__init__.py +++ b/pyignite/dbapi/__init__.py @@ -1,18 +1,17 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at # -# Copyright 2021 GridGain Systems, Inc. and Contributors. -# -# Licensed under the GridGain Community Edition License (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.gridgain.com/products/software/community-edition/gridgain-community-edition-license +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -# from .dbclient import DBClient from .errors import * diff --git a/pyignite/dbapi/dbclient.py b/pyignite/dbapi/dbclient.py index 220a05e..fcb85a8 100644 --- a/pyignite/dbapi/dbclient.py +++ b/pyignite/dbapi/dbclient.py @@ -1,18 +1,17 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at # -# Copyright 2021 GridGain Systems, Inc. and Contributors. -# -# Licensed under the GridGain Community Edition License (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.gridgain.com/products/software/community-edition/gridgain-community-edition-license +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -# from ..client import Client from .dbcursor import DBCursor diff --git a/pyignite/dbapi/dbcursor.py b/pyignite/dbapi/dbcursor.py index a6009c8..68c2b61 100644 --- a/pyignite/dbapi/dbcursor.py +++ b/pyignite/dbapi/dbcursor.py @@ -1,18 +1,17 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at # -# Copyright 2021 GridGain Systems, Inc. and Contributors. -# -# Licensed under the GridGain Community Edition License (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.gridgain.com/products/software/community-edition/gridgain-community-edition-license +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -# from ..cursors import SqlCursor diff --git a/pyignite/dbapi/errors.py b/pyignite/dbapi/errors.py index 07b315f..7bb5c06 100644 --- a/pyignite/dbapi/errors.py +++ b/pyignite/dbapi/errors.py @@ -1,18 +1,17 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at # -# Copyright 2021 GridGain Systems, Inc. and Contributors. -# -# Licensed under the GridGain Community Edition License (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.gridgain.com/products/software/community-edition/gridgain-community-edition-license +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -# class Warning(Exception): pass diff --git a/pyignite/dbapi/sqlalchemy.py b/pyignite/dbapi/sqlalchemy.py index 2decc90..a37d617 100644 --- a/pyignite/dbapi/sqlalchemy.py +++ b/pyignite/dbapi/sqlalchemy.py @@ -1,18 +1,17 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at # -# Copyright 2021 GridGain Systems, Inc. and Contributors. -# -# Licensed under the GridGain Community Edition License (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.gridgain.com/products/software/community-edition/gridgain-community-edition-license +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -# from sqlalchemy import types from sqlalchemy.engine import default