From 3917eccd252ea575da633433a4b8c59622c588f8 Mon Sep 17 00:00:00 2001 From: jiajunsu Date: Mon, 7 Nov 2022 21:05:01 +0800 Subject: [PATCH 1/2] Release openGauss-sqlalchemy --- .gitignore | 41 + LICENSE.md | 20 + README.en.md | 233 +++++- README.md | 235 +++++- opengauss_sqlalchemy/__init__.py | 20 + opengauss_sqlalchemy/base.py | 214 ++++++ opengauss_sqlalchemy/dc_psycopg2.py | 20 + opengauss_sqlalchemy/provision.py | 136 ++++ opengauss_sqlalchemy/psycopg2.py | 77 ++ opengauss_sqlalchemy/requirements.py | 1055 ++++++++++++++++++++++++++ setup.cfg | 16 + setup.py | 42 + test/__init__.py | 0 test/conftest.py | 3 + test/test_compiler.py | 646 ++++++++++++++++ test/test_suite.py | 872 +++++++++++++++++++++ tox.ini | 30 + 17 files changed, 3610 insertions(+), 50 deletions(-) create mode 100644 .gitignore create mode 100644 LICENSE.md create mode 100644 opengauss_sqlalchemy/__init__.py create mode 100644 opengauss_sqlalchemy/base.py create mode 100644 opengauss_sqlalchemy/dc_psycopg2.py create mode 100644 opengauss_sqlalchemy/provision.py create mode 100644 opengauss_sqlalchemy/psycopg2.py create mode 100644 opengauss_sqlalchemy/requirements.py create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 test/__init__.py create mode 100644 test/conftest.py create mode 100644 test/test_compiler.py create mode 100644 test/test_suite.py create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c566ded --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +*.pyc +*.pyd +*.pyo +*.egg +/build/ +/dist/ +/doc/build/output/ +/doc/build/_build/ +/dogpile_data/ +*.orig +*,cover +/.tox +/venv/ +.venv +*.egg-info +.coverage +coverage.xml +.*,cover +*.class +*.so +*.patch +sqlnet.log +/shard?_*.db +/test.cfg +/.cache/ +/.mypy_cache +*.sw[o,p] +*.rej +test/test_schema.db +*test_schema.db +.idea +/Pipfile* +/.pytest_cache/ +/pip-wheel-metadata/ +/.vscode/ +/.ipynb_checkpoints/ +*.ipynb +/querytest.db +/.mypy_cache +/.pytest_cache +/db_idents.txt diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..eebf3b5 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,20 @@ +Copyright 2005-2022 SQLAlchemy authors and contributors . +Copyright (C) 2021-2022 Huawei. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.en.md b/README.en.md index 140f836..7214fe6 100644 --- a/README.en.md +++ b/README.en.md @@ -1,36 +1,221 @@ # openGauss-sqlalchemy -#### Description -{**When you're done, you can delete the content in this README and update the file with details for others getting started with your repository**} +OpenGauss dialect for SQLAlchemy. -#### Software Architecture -Software architecture description +This project has been tested with test suites of SQLAlchemy. -#### Installation -1. xxxx -2. xxxx -3. xxxx +## Dependency for opengauss -#### Instructions +- psycopg2 for opengauss -1. xxxx -2. xxxx -3. xxxx + Download and install reference + > https://gitee.com/opengauss/openGauss-connector-python-psycopg2 -#### Contribution + or -1. Fork the repository -2. Create Feat_xxx branch -3. Commit your code -4. Create Pull Request + > https://github.com/opengauss-mirror/openGauss-connector-python-psycopg2 +## Installation -#### Gitee Feature +``` +>>> python setup.py install +``` -1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md -2. Gitee blog [blog.gitee.com](https://blog.gitee.com) -3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore) -4. The most valuable open source project [GVP](https://gitee.com/gvp) -5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help) -6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) +## Usage + +Create an DSN(Data Source Name) that points to your OpenGauss database. + +``` +>>> import sqlalchemy as sa +# With centralized mode +>>> sa.create_engine('opengauss://username:password@host:port/database_name') +# Or +>>> sa.create_engine('opengauss+psycopg2://username:password@host:port/database_name') +# With distributed mode +>>> sa.create_engine('opengauss+dc_psycopg2://username:password@host:port/database_name') +# Or +>>> sa.create_engine('opengauss+dc_psycopg2://username:password@/database_name?host=hostA:portA&host=hostB:portB') +``` + +See the [OpenGauss DeveloperGuide](https://docs.opengauss.org/en/docs/3.1.0/docs/BriefTutorial/BriefTutorial.html) for more infomation. + +## Features For Centralized and Distributed OpenGauss + +### Index + +- Index with `USING method` +``` +tbl = Table("testtbl", m, Column("data", String)) +Index("test_idx1", tbl.c.data, opengauss_using="btree") +``` + +- Index with column expression +``` +tbl = Table( + "testtbl", + m, + Column("data", String), + Column("data2", Integer, key="d2"), +) + +Index( + "test_idx1", + tbl.c.data, + tbl.c.d2, + opengauss_ops={"data": "text_pattern_ops", "d2": "int4_ops"}, +) +``` + +- Index with `LOCAL`, only available for index on a partitioned table +``` +tbl = Table( + "testtbl", + m, + Column("data", Integer), + opengauss_partition_by="RANGE (data) ..." +) +Index("test_idx1", tbl.c.data, opengauss_local=[""]) + +Index( + "test_idx2", + tbl.c.data, + opengauss_local=[ + "PARTITION data_index1", + "PARTITION data_index2 TABLESPACE example3", + ] +) +``` + +- Index with `WITH` +``` +tbl = Table("testtbl", m, Column("data", String)) +Index("test_idx1", tbl.c.data, opengauss_with={"fillfactor": 50}) +``` + +- Index with `TABLESPACE` +``` +tbl = Table("testtbl", m, Column("data", String)) +Index("test_idx1", tbl.c.data, opengauss_tablespace="sometablespace") +``` + +- Index with `WHERE`, unsupported for index on a partitioned table +``` +tbl = Table("testtbl", m, Column("data", Integer)) +Index( + "test_idx1", + tbl.c.data, + opengauss_where=and_(tbl.c.data > 5, tbl.c.data < 10), +) +``` + +### Table + +- Table with `WITH ({storage_parameter = value})` +``` +Table("some_table", ..., opengauss_with={"storage_parameter": "value"}) +``` + +- Table with `ON COMMIT` +``` +Table("some_talbe", ..., prefixes=["TEMPORARY"], opengauss_on_commit="PRESERVE ROWS") +``` + +- Table with `COMPRESS` +``` +Table("some_talbe", ..., opengauss_with={"ORIENTATION": "COLUMN"}, opengauss_compress=True) +``` + +- Table with `TABLESPACE tablespace_name` +``` +Table("some_talbe", ..., opengauss_tablespace="tablespace_name") +``` + +- Table with `PARTITION BY` +``` +Table("some_talbe", ..., opengauss_partition_by="RANGE(column_name) " + "(PARTITION P1 VALUES LESS THAN(10), " + "PARTITION P2 VALUES LESS THAN(MAXVALUE))") +``` + +- Table with `ENABLE ROW MOVEMENT` +``` +Table("some_talbe", ..., opengauss_partition_by="RANGE(column_name) ...", + opengauss_enable_row_movement=True) +``` + +## Features For Centralized OpenGauss + +### Index + +- Index with `CONCURRENTLY` +``` +tbl = Table("testtbl", m, Column("data", Integer)) +Index("test_idx1", tbl.c.data, opengauss_concurrently=True) +``` + +## Features For Distributed OpenGauss + +### TABLE + +- Table with `DISTRIBUTE BY` +``` +Table("some_table", ..., opengauss_distribute_by="HASH(column_name)") +``` +NOTE: table without distributable columns will be set with "DISTRIBUTE BY REPLICATION" + +- Table with `TO GROUP` +``` +Table("some_table", ..., opengauss_to="GROUP group_name") +``` + + +## Releasing + +### Build python wheel +``` +>>> pip install wheel +>>> python setup.py bdist_wheel +``` + +### Testing + +1. Set environment with `export LD_LIBRARY_PATH=` and `export PYTHONPATH=` to your path of package `psycopg2`. +2. Install opengauss and update configuration, see "Steps to install and config opengauss for testing". +3. Execute `tox -e py38`. + + +### Steps to install and config centralized opengauss for testing + +1. Add OS user for opengauss ```>>> useradd omm -g dbgrp``` +2. Change owner of opengass dir ```>>> chown omm:dbgrp ${db_dir} -R``` +3. Switch to user omm ```>>> su - omm``` +4. Install opengauss ```>>> sh install.sh -w ${db_password} -p 37200``` +5. Start opengauss ```>>> gs_ctl start -D ${db_dir}/data/single_node/``` +6. Login opengauss ```>>> gsql -d postgres -p 37200``` +7. Create database user and create database & schema for testing +``` +openGauss=# create user scott identified by 'Tiger123'; +openGauss=# create database test with owner=scott encoding='utf8' template=template0; +openGauss=# GRANT ALL PRIVILEGES TO scott; +openGauss=# ALTER DATABASE test SET default_text_search_config = 'pg_catalog.english'; +openGauss=# \c test +test=# create schema test_schema AUTHORIZATION scott; +test=# create schema test_schema_2 AUTHORIZATION scott; +test=# \q +``` +8. Config opengauss +``` +>>> gs_guc set -D ${db_dir}/data/single_node/ -c "ssl=off" +>>> gs_guc set -D ${db_dir}/data/single_node/ -c "max_prepared_transactions = 100" +>>> gs_guc reload -D ${db_dir}/data/single_node/ -h "local all scott sha256" +>>> gs_guc reload -D ${db_dir}/data/single_node/ -h "host all scott 127.0.0.1/32 sha256" +>>> gs_guc reload -D ${db_dir}/data/single_node/ -h "host all scott 0.0.0.0/0 sha256" +>>> gs_ctl stop -D ${db_dir}/data/single_node/ +>>> gs_tl start -D ${db_dir}/data/single_node/ +``` +9. Optional: enable log_statement +``` +>>> gs_guc set -D ${db_dir}/data/single_node/ -c "log_min_error_statement = error" +>>> gs_guc set -D ${db_dir}/data/single_node/ -c "log_statement = 'all'" +``` diff --git a/README.md b/README.md index d9e40c0..3cf2405 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,222 @@ # openGauss-sqlalchemy -#### 介绍 -{**以下是 Gitee 平台说明,您可以替换此简介** -Gitee 是 OSCHINA 推出的基于 Git 的代码托管平台(同时支持 SVN)。专为开发者提供稳定、高效、安全的云端软件开发协作平台 -无论是个人、团队、或是企业,都能够用 Gitee 实现代码托管、项目管理、协作开发。企业项目请看 [https://gitee.com/enterprises](https://gitee.com/enterprises)} +适配SQLAlchemy框架的OpenGauss方言库。 -#### 软件架构 -软件架构说明 +项目已通过SQLAlchemy社区用例集。 -#### 安装教程 +## 运行依赖 -1. xxxx -2. xxxx -3. xxxx +- psycopg2 for opengauss -#### 使用说明 + 从下列地址下载并安装到运行环境中 + > https://gitee.com/opengauss/openGauss-connector-python-psycopg2 -1. xxxx -2. xxxx -3. xxxx + 或 -#### 参与贡献 + > https://github.com/opengauss-mirror/openGauss-connector-python-psycopg2 -1. Fork 本仓库 -2. 新建 Feat_xxx 分支 -3. 提交代码 -4. 新建 Pull Request +## 安装 -#### 特技 +``` +>>> python setup.py install +``` -1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md -2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com) -3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目 -4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目 -5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) -6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) +## 使用方法 + +在OpenGauss中创建一个数据库。 + +``` +>>> import sqlalchemy as sa +# 访问集中式模式DB +>>> sa.create_engine('opengauss://username:password@host:port/database_name') +# 或 +>>> sa.create_engine('opengauss+psycopg2://username:password@host:port/database_name') +# 访问分布式模式DB +>>> sa.create_engine('opengauss+dc_psycopg2://username:password@host:port/database_name') +# 或 +>>> sa.create_engine('opengauss+dc_psycopg2://username:password@/database_name?host=hostA:portA&host=hostB:portB') +``` + +OpenGauss的数据库开发指南详见 [OpenGauss DeveloperGuide](https://docs.opengauss.org/zh/docs/latest/docs/Developerguide/Developerguide.html)。 + +## OpenGauss特性的使用方式(集中式和分布式) + +### 索引 + +- Index with `USING method` +``` +tbl = Table("testtbl", m, Column("data", String)) +Index("test_idx1", tbl.c.data, opengauss_using="btree") +``` + +- Index with column expression +``` +tbl = Table( + "testtbl", + m, + Column("data", String), + Column("data2", Integer, key="d2"), +) + +Index( + "test_idx1", + tbl.c.data, + tbl.c.d2, + opengauss_ops={"data": "text_pattern_ops", "d2": "int4_ops"}, +) +``` + +- Index with `LOCAL`, only available for index on a partitioned table +``` +tbl = Table( + "testtbl", + m, + Column("data", Integer), + opengauss_partition_by="RANGE (data) ..." +) +Index("test_idx1", tbl.c.data, opengauss_local=[""]) + +Index( + "test_idx2", + tbl.c.data, + opengauss_local=[ + "PARTITION data_index1", + "PARTITION data_index2 TABLESPACE example3", + ] +) +``` + +- Index with `WITH` +``` +tbl = Table("testtbl", m, Column("data", String)) +Index("test_idx1", tbl.c.data, opengauss_with={"fillfactor": 50}) +``` + +- Index with `TABLESPACE` +``` +tbl = Table("testtbl", m, Column("data", String)) +Index("test_idx1", tbl.c.data, opengauss_tablespace="sometablespace") +``` + +- Index with `WHERE`, unsupported for index on a partitioned table +``` +tbl = Table("testtbl", m, Column("data", Integer)) +Index( + "test_idx1", + tbl.c.data, + opengauss_where=and_(tbl.c.data > 5, tbl.c.data < 10), +) +``` + +### 表 + +- Table with `WITH ({storage_parameter = value})` +``` +Table("some_table", ..., opengauss_with={"storage_parameter": "value"}) +``` + +- Table with `ON COMMIT` +``` +Table("some_talbe", ..., prefixes=["TEMPORARY"], opengauss_on_commit="PRESERVE ROWS") +``` + +- Table with `COMPRESS` +``` +Table("some_talbe", ..., opengauss_with={"ORIENTATION": "COLUMN"}, opengauss_compress=True) +``` + +- Table with `TABLESPACE tablespace_name` +``` +Table("some_talbe", ..., opengauss_tablespace="tablespace_name") +``` + +- Table with `PARTITION BY` +``` +Table("some_talbe", ..., opengauss_partition_by="RANGE(column_name) " + "(PARTITION P1 VALUES LESS THAN(10), " + "PARTITION P2 VALUES LESS THAN(MAXVALUE))") +``` + +- Table with `ENABLE ROW MOVEMENT` +``` +Table("some_talbe", ..., opengauss_partition_by="RANGE(column_name) ...", + opengauss_enable_row_movement=True) +``` + +## OpenGauss特性的使用方式(集中式) + +### 索引 + +- Index with `CONCURRENTLY` +``` +tbl = Table("testtbl", m, Column("data", Integer)) +Index("test_idx1", tbl.c.data, opengauss_concurrently=True) +``` + +## OpenGauss特性的使用方式(分布式) + +### 表 + +- Table with `DISTRIBUTE BY` +``` +Table("some_table", ..., opengauss_distribute_by="HASH(column_name)") +``` +NOTE: table without distributable columns will be set with "DISTRIBUTE BY REPLICATION" + +- Table with `TO GROUP` +``` +Table("some_table", ..., opengauss_to="GROUP group_name") +``` + + +## 发布指南 + +### 构建 python wheel格式 +``` +>>> pip install wheel +>>> python setup.py bdist_wheel +``` + +### 本地测试 + +1. 设置环境变量 `export LD_LIBRARY_PATH=` 和 `export PYTHONPATH=` 的值为测试环境中 `psycopg2` 包所在的目录. +2. 安装OpenGauss并修改数据库配置, 具体步骤见 "安装并配置OpenGauss调测环境". +3. 执行命令 `tox -e py38`. + + +### 安装并配置OpenGauss调测环境 + +1. 添加OpenGauss的操作系统用户 ```>>> useradd omm -g dbgrp``` +2. 修改OpenGauss目录的用户和用户组 ```>>> chown omm:dbgrp ${db_dir} -R``` +3. 切换到新的系统用户 ```>>> su - omm``` +4. 安装OpenGauss ```>>> sh install.sh -w ${db_password} -p 37200``` +5. 启动OpenGauss ```>>> gs_ctl start -D ${db_dir}/data/single_node/``` +6. 登录OpenGauss ```>>> gsql -d postgres -p 37200``` +7. 创建数据库用户、测试数据库和测试模式 +``` +openGauss=# create user scott identified by 'Tiger123'; +openGauss=# create database test with owner=scott encoding='utf8' template=template0; +openGauss=# GRANT ALL PRIVILEGES TO scott; +openGauss=# ALTER DATABASE test SET default_text_search_config = 'pg_catalog.english'; +openGauss=# \c test +test=# create schema test_schema AUTHORIZATION scott; +test=# create schema test_schema_2 AUTHORIZATION scott; +test=# \q +``` +8. 修改OpenGauss配置 +``` +>>> gs_guc set -D ${db_dir}/data/single_node/ -c "ssl=off" +>>> gs_guc set -D ${db_dir}/data/single_node/ -c "max_prepared_transactions = 100" +>>> gs_guc reload -D ${db_dir}/data/single_node/ -h "local all scott sha256" +>>> gs_guc reload -D ${db_dir}/data/single_node/ -h "host all scott 127.0.0.1/32 sha256" +>>> gs_guc reload -D ${db_dir}/data/single_node/ -h "host all scott 0.0.0.0/0 sha256" +>>> gs_ctl stop -D ${db_dir}/data/single_node/ +>>> gs_tl start -D ${db_dir}/data/single_node/ +``` +9. 启用SQL日志记录(可选) +``` +>>> gs_guc set -D ${db_dir}/data/single_node/ -c "log_min_error_statement = error" +>>> gs_guc set -D ${db_dir}/data/single_node/ -c "log_statement = 'all'" +``` diff --git a/opengauss_sqlalchemy/__init__.py b/opengauss_sqlalchemy/__init__.py new file mode 100644 index 0000000..1a16f00 --- /dev/null +++ b/opengauss_sqlalchemy/__init__.py @@ -0,0 +1,20 @@ +# opengauss_sqlalchemy/__init__.py +# Copyright (C) 2021-2022 Huawei. +# This module is part of SQLAlchemy and is released under +# the MIT License: https://www.opensource.org/licenses/mit-license.php + +from sqlalchemy.dialects import registry + + +__version__ = "2.0" + + +registry.register( + "opengauss", "opengauss_sqlalchemy.psycopg2", "OpenGaussDialect_psycopg2" +) +registry.register( + "opengauss.psycopg2", "opengauss_sqlalchemy.psycopg2", "OpenGaussDialect_psycopg2" +) +registry.register( + "opengauss.dc_psycopg2", "opengauss_sqlalchemy.dc_psycopg2", "OpenGaussDialect_dc_psycopg2" +) diff --git a/opengauss_sqlalchemy/base.py b/opengauss_sqlalchemy/base.py new file mode 100644 index 0000000..933516e --- /dev/null +++ b/opengauss_sqlalchemy/base.py @@ -0,0 +1,214 @@ +# opengauss_sqlalchemy/base.py +# Copyright (C) 2005-2022 the SQLAlchemy authors and contributors +# +# This source file has been modified by Huawei. +# Copyright (C) 2021-2022 Huawei. +# +# This module is part of SQLAlchemy and is released under +# the MIT License: https://www.opensource.org/licenses/mit-license.php + +from sqlalchemy.dialects.postgresql.base import IDX_USING, PGDDLCompiler, PGIdentifierPreparer +from sqlalchemy.dialects.postgresql.base import RESERVED_WORDS as _RESERVED_WORDS +from sqlalchemy.sql import coercions, expression, roles +from sqlalchemy import types + + +_distributable_types = ( + types.BIGINT, types.BigInteger, types.INT, types.INTEGER, + types.Integer, types.NUMERIC, types.Numeric, types.SMALLINT, + types.SmallInteger, types.DECIMAL, + types.CHAR, types.NVARCHAR, types.String, types.TEXT, + types.Text, types.Unicode, types.UnicodeText, types.VARCHAR, + types.DATE, types.DATETIME, types.Date, types.DateTime, + types.Interval, types.TIME, types.TIMESTAMP, types.Time, +) + +_undistributable_types = ( + "FLOAT", # FLOAT isinstance of types.Numeric/DECIMAL +) + +# Ref: https://opengauss.org/en/docs/3.1.0/docs/Developerguide/keywords.html +RESERVED_WORDS = _RESERVED_WORDS.union( + [ + "authid", + "buckets", + "excluded", + "fenced", + "groupparent", + "is", + "less", + "maxvalue", + "minus", + "modify", + "performance", + "priorer", + "procedure", + "reject", + "rownum", + "sysdate", + "verify", + ] +) + + +class OpenGaussDDLCompiler(PGDDLCompiler): + """DDLCompiler for opengauss""" + + def visit_create_index(self, create): + preparer = self.preparer + index = create.element + self._verify_index_table(index) + text_contents = ["CREATE "] + if index.unique: + text_contents.append("UNIQUE ") + text_contents.append("INDEX ") + + if self.dialect._supports_create_index_concurrently: + concurrently = index.dialect_options["opengauss"]["concurrently"] + if concurrently: + text_contents.append("CONCURRENTLY ") + + text_contents.append( + "%s ON %s " % ( + self._prepared_index_name(index, include_schema=False), + preparer.format_table(index.table), + ) + ) + + using = index.dialect_options["opengauss"]["using"] + if using: + text_contents.append( + "USING %s " + % self.preparer.validate_sql_phrase(using, IDX_USING).lower() + ) + + ops = index.dialect_options["opengauss"]["ops"] + text_contents.append( + "(%s)" % ( + ", ".join( + [ + self.sql_compiler.process( + expr.self_group() + if not isinstance(expr, expression.ColumnClause) else expr, + include_table=False, + literal_binds=True, + ) + + ( + (" " + ops[expr.key]) + if hasattr(expr, "key") and expr.key in ops else "" + ) + for expr in index.expressions + ] + ) + ) + ) + + local = index.dialect_options["opengauss"]["local"] + if local: + text_contents.append(" LOCAL") + if local[0]: + text_contents.append( + " (%s)" % ( + ", ".join([local_partition for local_partition in local]) + ) + ) + + withclause = index.dialect_options["opengauss"]["with"] + if withclause: + text_contents.append( + " WITH (%s)" % ( + ", ".join( + [ + "%s = %s" % storage_parameter + for storage_parameter in withclause.items() + ] + ) + ) + ) + + tablespace_name = index.dialect_options["opengauss"]["tablespace"] + if tablespace_name: + text_contents.append(" TABLESPACE %s" % preparer.quote(tablespace_name)) + + whereclause = index.dialect_options["opengauss"]["where"] + if whereclause is not None: + whereclause = coercions.expect( + roles.DDLExpressionRole, whereclause + ) + + where_compiled = self.sql_compiler.process( + whereclause, include_table=False, literal_binds=True + ) + text_contents.append(" WHERE " + where_compiled) + + return "".join(text_contents) + + def visit_drop_index(self, drop): + index = drop.element + + text_contents = ["\nDROP INDEX "] + + if self.dialect._supports_drop_index_concurrently: + concurrently = index.dialect_options["opengauss"]["concurrently"] + if concurrently: + text_contents.append("CONCURRENTLY ") + + if drop.if_exists: + text_contents.append("IF EXISTS ") + + text_contents.append(self._prepared_index_name(index, include_schema=True)) + return "".join(text_contents) + + def post_create_table(self, table): + table_opts = [] + gauss_opts = table.dialect_options["opengauss"] + + if gauss_opts["with"]: + table_opts.append( + "\n WITH (%s)" % ( + ", ".join( + ["%s = %s" % storage_parameter for storage_parameter in gauss_opts["with"].items()] + ) + ) + ) + + if gauss_opts["on_commit"]: + table_opts.append("\n ON COMMIT %s" % gauss_opts["on_commit"]) + + if gauss_opts["compress"]: + table_opts.append("\n COMPRESS") + + if gauss_opts["tablespace"]: + tablespace_name = gauss_opts["tablespace"] + table_opts.append( + "\n TABLESPACE %s" % self.preparer.quote(tablespace_name) + ) + + if self.dialect._supports_table_distribute_by: + if gauss_opts["distribute_by"]: + # Support DISTRIBUTE BY in distributed opengauss. + # Usage: `Table("some_table", opengauss_distribute_by='HASH(column_name)')` + # See https://support.huaweicloud.com/devg-opengauss/opengauss_devg_0402.html + table_opts.append("\n DISTRIBUTE BY %s" % gauss_opts["distribute_by"]) + else: + for col in table.columns: + if isinstance(col.type, _distributable_types) and str(col.type) not in _undistributable_types: + break + else: + # treat table without distributable columns as REPLICATION in distributed opengauss + table_opts.append("\n DISTRIBUTE BY REPLICATION") + + if gauss_opts["to"]: + table_opts.append("\n TO %s" % gauss_opts["to"]) + + if gauss_opts["partition_by"]: + table_opts.append("\n PARTITION BY %s" % gauss_opts["partition_by"]) + + if gauss_opts["enable_row_movement"]: + table_opts.append("\n ENABLE ROW MOVEMENT") + + return "".join(table_opts) + + +class OpenGaussIdentifierPreparer(PGIdentifierPreparer): + reserved_words = RESERVED_WORDS diff --git a/opengauss_sqlalchemy/dc_psycopg2.py b/opengauss_sqlalchemy/dc_psycopg2.py new file mode 100644 index 0000000..328b9f0 --- /dev/null +++ b/opengauss_sqlalchemy/dc_psycopg2.py @@ -0,0 +1,20 @@ +# opengauss_sqlalchemy/dc_psycopg2.py +# Copyright (C) 2021-2022 Huawei. +# This module is part of SQLAlchemy and is released under +# the MIT License: https://www.opensource.org/licenses/mit-license.php + +from opengauss_sqlalchemy.psycopg2 import OpenGaussDialect_psycopg2 + + +class OpenGaussDialect_dc_psycopg2(OpenGaussDialect_psycopg2): + name = "opengauss" + driver = "dc_psycopg2" + + supports_statement_cache = True + + _supports_create_index_concurrently = False + _supports_drop_index_concurrently = False + _supports_table_distribute_by = True + + +dialect = OpenGaussDialect_dc_psycopg2 diff --git a/opengauss_sqlalchemy/provision.py b/opengauss_sqlalchemy/provision.py new file mode 100644 index 0000000..6f21d0f --- /dev/null +++ b/opengauss_sqlalchemy/provision.py @@ -0,0 +1,136 @@ +# opengauss_sqlalchemy/provision.py +# Copyright (C) 2005-2022 the SQLAlchemy authors and contributors +# +# This source file has been modified by Huawei. +# Copyright (C) 2021-2022 Huawei. +# +# This module is part of SQLAlchemy and is released under +# the MIT License: https://www.opensource.org/licenses/mit-license.php + +import time + +from sqlalchemy import exc +from sqlalchemy import inspect +from sqlalchemy import text +from sqlalchemy.testing import warn_test_suite +from sqlalchemy.testing.provision import create_db +from sqlalchemy.testing.provision import drop_all_schema_objects_post_tables +from sqlalchemy.testing.provision import drop_all_schema_objects_pre_tables +from sqlalchemy.testing.provision import drop_db +from sqlalchemy.testing.provision import log +from sqlalchemy.testing.provision import prepare_for_drop_tables +from sqlalchemy.testing.provision import set_default_schema_on_connection +from sqlalchemy.testing.provision import temp_table_keyword_args + + +@create_db.for_db("opengauss") +def _opengauss_create_db(cfg, eng, ident): + template_db = cfg.options.opengauss_templatedb + + with eng.execution_options(isolation_level="AUTOCOMMIT").begin() as conn: + try: + _opengauss_drop_db(cfg, conn, ident) + except Exception: + pass + if not template_db: + template_db = conn.exec_driver_sql( + "select current_database()" + ).scalar() + + attempt = 0 + while True: + try: + conn.exec_driver_sql( + "CREATE DATABASE %s TEMPLATE %s" % (ident, template_db) + ) + except exc.OperationalError as err: + attempt += 1 + if attempt >= 3: + raise + if "accessed by other users" in str(err): + log.info( + "Waiting to create %s, URI %r, " + "template DB %s is in use sleeping for .5", + ident, + eng.url, + template_db, + ) + time.sleep(0.5) + else: + break + + +@drop_db.for_db("opengauss") +def _opengauss_drop_db(cfg, eng, ident): + with eng.connect().execution_options(isolation_level="AUTOCOMMIT") as conn: + with conn.begin(): + conn.execute( + text( + "select pg_terminate_backend(pid) from pg_stat_activity " + "where usename=current_user and pid != pg_backend_pid() " + "and datname=:dname" + ), + dict(dname=ident), + ) + conn.exec_driver_sql("DROP DATABASE %s" % ident) + + +@temp_table_keyword_args.for_db("opengauss") +def _opengauss_temp_table_keyword_args(cfg, eng): + return {"prefixes": ["TEMPORARY"]} + + +@set_default_schema_on_connection.for_db("opengauss") +def _opengauss_set_default_schema_on_connection( + cfg, dbapi_connection, schema_name +): + existing_autocommit = dbapi_connection.autocommit + dbapi_connection.autocommit = True + cursor = dbapi_connection.cursor() + cursor.execute("SET SESSION search_path='%s'" % schema_name) + cursor.close() + dbapi_connection.autocommit = existing_autocommit + + +@drop_all_schema_objects_pre_tables.for_db("opengauss") +def drop_all_schema_objects_pre_tables(cfg, eng): + with eng.connect().execution_options(isolation_level="AUTOCOMMIT") as conn: + for xid in conn.exec_driver_sql( + "select gid from pg_prepared_xacts" + ).scalars(): + conn.execute("ROLLBACK PREPARED '%s'" % xid) + + +@drop_all_schema_objects_post_tables.for_db("opengauss") +def drop_all_schema_objects_post_tables(cfg, eng): + from sqlalchemy.dialects import postgresql + + inspector = inspect(eng) + with eng.begin() as conn: + for enum in inspector.get_enums("*"): + conn.execute( + postgresql.DropEnumType( + postgresql.ENUM(name=enum["name"], schema=enum["schema"]) + ) + ) + + +@prepare_for_drop_tables.for_db("opengauss") +def prepare_for_drop_tables(config, connection): + """Ensure there are no locks on the current username/database.""" + + result = connection.exec_driver_sql( + "select pid, state, wait_event_type, query " + # "select pg_terminate_backend(pid), state, wait_event_type " + "from pg_stat_activity where " + "usename=current_user " + "and datname=current_database() and state='idle in transaction' " + "and pid != pg_backend_pid()" + ) + rows = result.all() # noqa + if rows: + warn_test_suite( + "OpenGauss may not be able to DROP tables due to " + "idle in transaction: %s" + % ("; ".join(row._mapping["query"] for row in rows)) + ) diff --git a/opengauss_sqlalchemy/psycopg2.py b/opengauss_sqlalchemy/psycopg2.py new file mode 100644 index 0000000..50c275f --- /dev/null +++ b/opengauss_sqlalchemy/psycopg2.py @@ -0,0 +1,77 @@ +# opengauss_sqlalchemy/psycopg2.py +# Copyright (C) 2021-2022 Huawei. +# This module is part of SQLAlchemy and is released under +# the MIT License: https://www.opensource.org/licenses/mit-license.php + +from sqlalchemy.dialects.postgresql.psycopg2 import PGCompiler_psycopg2, PGDialect_psycopg2 +from sqlalchemy import schema +from sqlalchemy import util + +from opengauss_sqlalchemy.base import OpenGaussDDLCompiler, OpenGaussIdentifierPreparer + + +class OpenGaussCompiler_psycopg2(PGCompiler_psycopg2): + def get_cte_preamble(self, recursive): + return "WITH RECURSIVE" + + +class OpenGaussDialect_psycopg2(PGDialect_psycopg2): + name = "opengauss" + driver = "psycopg2" + + cte_follows_insert = True + supports_statement_cache = True + + statement_compiler = OpenGaussCompiler_psycopg2 + ddl_compiler = OpenGaussDDLCompiler + preparer = OpenGaussIdentifierPreparer + + construct_arguments = [ + ( + schema.Index, + { + "concurrently": False, + "using": None, + "ops": {}, + "local": [], + "with": {}, + "tablespace": None, + "where": None, + }, + ), + ( + schema.Table, + { + "ignore_search_path": False, + "with": {}, + "on_commit": None, + "compress": False, + "tablespace": None, + "distribute_by": None, + "to": None, + "partition_by": None, + "enable_row_movement": False, + }, + ), + ] + + _supports_table_distribute_by = False + + @util.memoized_property + def _isolation_lookup(self): + extensions = self._psycopg2_extensions() + + return { + "AUTOCOMMIT": extensions.ISOLATION_LEVEL_AUTOCOMMIT, + "READ COMMITTED": extensions.ISOLATION_LEVEL_READ_COMMITTED, + "READ UNCOMMITTED": extensions.ISOLATION_LEVEL_READ_UNCOMMITTED, + "REPEATABLE READ": extensions.ISOLATION_LEVEL_REPEATABLE_READ, + # opengauss does NOT support SERIALIZABLE + } + + def _get_server_version_info(self, connection): + # most of opengauss features are same with postgres 9.2.4 + return (9, 2, 4) + + +dialect = OpenGaussDialect_psycopg2 diff --git a/opengauss_sqlalchemy/requirements.py b/opengauss_sqlalchemy/requirements.py new file mode 100644 index 0000000..0a38baf --- /dev/null +++ b/opengauss_sqlalchemy/requirements.py @@ -0,0 +1,1055 @@ +# sqlalchemy_opengauss/requirements.py +# Copyright (C) 2005-2022 the SQLAlchemy authors and contributors +# +# This source file has been modified by Huawei. +# Copyright (C) 2021-2022 Huawei. +# +# This module is part of SQLAlchemy and is released under +# the MIT License: https://www.opensource.org/licenses/mit-license.php + +import sys + +from sqlalchemy.testing.requirements import SuiteRequirements + +from sqlalchemy.testing import exclusions + + +class Requirements(SuiteRequirements): + @property + def deferrable_or_no_constraints(self): + """Target database must support deferrable constraints.""" + + return exclusions.open() + + @property + def check_constraints(self): + """Target database must support check constraints.""" + + return exclusions.open() + + @property + def enforces_check_constraints(self): + """Target database must also enforce check constraints.""" + + return exclusions.open() + + @property + def named_constraints(self): + """target database must support names for constraints.""" + + return exclusions.open() + + @property + def implicitly_named_constraints(self): + """target database must apply names to unnamed constraints.""" + + return exclusions.open() + + @property + def foreign_keys(self): + """Target database must support foreign keys.""" + return exclusions.skip_if("opengauss+dc_psycopg2") + + @property + def foreign_key_constraint_reflection(self): + return self.foreign_keys + + @property + def self_referential_foreign_keys(self): + return self.foreign_keys + + @property + def foreign_key_constraint_name_reflection(self): + return self.foreign_keys + + @property + def table_ddl_if_exists(self): + """target platform supports IF NOT EXISTS / IF EXISTS for tables.""" + + return exclusions.open() + + @property + def index_ddl_if_exists(self): + """target platform supports IF NOT EXISTS / IF EXISTS for indexes.""" + + return exclusions.closed() + + @property + def on_update_cascade(self): + """target database must support ON UPDATE..CASCADE behavior in + foreign keys.""" + + return self.foreign_keys + + @property + def non_updating_cascade(self): + """target database must *not* support ON UPDATE..CASCADE behavior in + foreign keys.""" + + return self.foreign_keys + + @property + def recursive_fk_cascade(self): + """target database must support ON DELETE CASCADE on a self-referential + foreign key""" + + return self.foreign_keys + + @property + def deferrable_fks(self): + """target database must support deferrable fks""" + + return self.foreign_keys + + @property + def foreign_key_constraint_option_reflection_ondelete(self): + return self.foreign_keys + + @property + def fk_constraint_option_reflection_ondelete_restrict(self): + return self.foreign_keys + + @property + def fk_constraint_option_reflection_ondelete_noaction(self): + return self.foreign_keys + + @property + def foreign_key_constraint_option_reflection_onupdate(self): + return self.foreign_keys + + @property + def fk_constraint_option_reflection_onupdate_restrict(self): + return self.foreign_keys + + @property + def comment_reflection(self): + return exclusions.open() + + @property + def unbounded_varchar(self): + """Target database must support VARCHAR with no length""" + + return exclusions.open() + + @property + def boolean_col_expressions(self): + """Target database must support boolean expressions as columns""" + return exclusions.open() + + @property + def non_native_boolean_unconstrained(self): + """target database is not native boolean and allows arbitrary integers + in it's "bool" column""" + + return exclusions.open() + + @property + def standalone_binds(self): + """target database/driver supports bound parameters as column expressions + without being in the context of a typed column. + + """ + return exclusions.open() + + @property + def qmark_paramstyle(self): + return exclusions.closed() + + @property + def named_paramstyle(self): + return exclusions.closed() + + @property + def format_paramstyle(self): + return exclusions.closed() + + @property + def pyformat_paramstyle(self): + return exclusions.open() + + @property + def no_quoting_special_bind_names(self): + """Target database will quote bound parameter names, doesn't support + EXPANDING""" + + return exclusions.open() + + @property + def temporary_tables(self): + """target database supports temporary tables""" + return exclusions.open() + + @property + def temp_table_reflection(self): + return self.temporary_tables + + @property + def temp_table_reflect_indexes(self): + return exclusions.open() + + @property + def reflectable_autoincrement(self): + """Target database must support tables that can automatically generate + PKs assuming they were reflected. + + this is essentially all the DBs in "identity" plus PostgreSQL, which + has SERIAL support. FB and Oracle (and sybase?) require the Sequence + to be explicitly added, including if the table was reflected. + """ + return exclusions.open() + + @property + def insert_from_select(self): + return exclusions.open() + + @property + def fetch_rows_post_commit(self): + return exclusions.open() + + @property + def non_broken_binary(self): + """target DBAPI must work fully with binary values""" + + return exclusions.open() + + @property + def binary_comparisons(self): + """target database/driver can allow BLOB/BINARY fields to be compared + against a bound parameter value. + """ + return exclusions.open() + + @property + def binary_literals(self): + """target backend supports simple binary literals, e.g. an + expression like:: + + SELECT CAST('foo' AS BINARY) + + Where ``BINARY`` is the type emitted from :class:`.LargeBinary`, + e.g. it could be ``BLOB`` or similar. + + Basically fails on Oracle. + + """ + + return exclusions.open() + + @property + def tuple_in(self): + return exclusions.open() + + @property + def tuple_in_w_empty(self): + return exclusions.open() + + @property + def independent_cursors(self): + """Target must support simultaneous, independent database cursors + on a single connection.""" + + return exclusions.open() + + @property + def cursor_works_post_rollback(self): + """Driver quirk where the cursor.fetchall() will work even if + the connection has been rolled back. + + This generally refers to buffered cursors but also seems to work + with cx_oracle, for example. + + """ + + return exclusions.open() + + @property + def independent_connections(self): + """ + Target must support simultaneous, independent database connections. + """ + + return exclusions.open() + + @property + def memory_process_intensive(self): + """Driver is able to handle the memory tests which run in a subprocess + and iterate through hundreds of connections + + """ + return exclusions.open() + + @property + def updateable_autoincrement_pks(self): + """Target must support UPDATE on autoincrement/integer primary key.""" + + return exclusions.open() + + @property + def isolation_level(self): + return exclusions.open() + + @property + def legacy_isolation_level(self): + return exclusions.open() + + def get_isolation_levels(self, config): + levels = set(config.db.dialect._isolation_lookup) + default = "READ COMMITTED" + levels.add("AUTOCOMMIT") + + return {"default": default, "supported": levels} + + @property + def autocommit(self): + """target dialect supports 'AUTOCOMMIT' as an isolation_level""" + + return exclusions.open() + + @property + def row_triggers(self): + """Target must support standard statement-running EACH ROW triggers.""" + + return exclusions.open() + + @property + def sequences_as_server_defaults(self): + """Target database must support SEQUENCE as a server side default.""" + + return exclusions.open() + + @property + def sql_expressions_inserted_as_primary_key(self): + return exclusions.open() + + @property + def computed_columns_on_update_returning(self): + return exclusions.closed() + + @property + def correlated_outer_joins(self): + """Target must support an outer join to a subquery which + correlates to the parent.""" + + return exclusions.open() + + @property + def multi_table_update(self): + return exclusions.closed() + + @property + def update_from(self): + """Target must support UPDATE..FROM syntax""" + + return exclusions.open() + + @property + def delete_from(self): + """Target must support DELETE FROM..FROM or DELETE..USING syntax""" + return exclusions.open() + + @property + def update_where_target_in_subquery(self): + """Target must support UPDATE (or DELETE) where the same table is + present in a subquery in the WHERE clause. + + This is an ANSI-standard syntax that apparently MySQL can't handle, + such as:: + + UPDATE documents SET flag=1 WHERE documents.title IN + (SELECT max(documents.title) AS title + FROM documents GROUP BY documents.user_id + ) + + """ + return exclusions.open() + + @property + def savepoints(self): + """Target database must support savepoints.""" + + return exclusions.open() + + @property + def savepoints_w_release(self): + return exclusions.open() + + @property + def schemas(self): + """Target database must support external schemas, and have one + named 'test_schema'.""" + + return exclusions.open() + + @property + def cross_schema_fk_reflection(self): + """target system must support reflection of inter-schema foreign + keys""" + return self.foreign_keys + + @property + def implicit_default_schema(self): + """target system has a strong concept of 'default' schema that can + be referred to implicitly. + + basically, PostgreSQL. + + """ + return exclusions.open() + + @property + def default_schema_name_switch(self): + return exclusions.open() + + @property + def unique_constraint_reflection(self): + return exclusions.open() + + @property + def unique_constraint_reflection_no_index_overlap(self): + return exclusions.open() + + @property + def check_constraint_reflection(self): + return exclusions.open() + + @property + def indexes_with_expressions(self): + return exclusions.open() + + @property + def temp_table_names(self): + """target dialect supports listing of temporary table names""" + + return exclusions.closed() + + @property + def temporary_views(self): + """target database supports temporary views""" + return exclusions.open() + + @property + def table_value_constructor(self): + return exclusions.open() + + @property + def update_nowait(self): + """Target database must support SELECT...FOR UPDATE NOWAIT""" + return exclusions.open() + + @property + def subqueries(self): + """Target database must support subqueries.""" + return exclusions.open() + + @property + def ctes(self): + """Target database supports CTEs""" + return exclusions.open() + + @property + def ctes_with_update_delete(self): + """target database supports CTES that ride on top of a normal UPDATE + or DELETE statement which refers to the CTE in a correlated subquery. + + """ + return exclusions.open() + + @property + def ctes_on_dml(self): + """target database supports CTES which consist of INSERT, UPDATE + or DELETE *within* the CTE, e.g. WITH x AS (UPDATE....)""" + + return exclusions.open() + + @property + def mod_operator_as_percent_sign(self): + """target database must use a plain percent '%' as the 'modulus' + operator.""" + + return exclusions.open() + + @property + def intersect(self): + """Target database must support INTERSECT or equivalent.""" + + return exclusions.open() + + @property + def except_(self): + """Target database must support EXCEPT or equivalent (i.e. MINUS).""" + return exclusions.open() + + @property + def dupe_order_by_ok(self): + """target db wont choke if ORDER BY specifies the same expression + more than once + + """ + + return exclusions.open() + + @property + def order_by_col_from_union(self): + """target database supports ordering by a column from a SELECT + inside of a UNION + + E.g. (SELECT id, ...) UNION (SELECT id, ...) ORDER BY id + + Fails on SQL Server + + """ + return exclusions.open() + + @property + def parens_in_union_contained_select_w_limit_offset(self): + """Target database must support parenthesized SELECT in UNION + when LIMIT/OFFSET is specifically present. + + E.g. (SELECT ... LIMIT ..) UNION (SELECT .. OFFSET ..) + + This is known to fail on SQLite. + + """ + return exclusions.open() + + @property + def parens_in_union_contained_select_wo_limit_offset(self): + """Target database must support parenthesized SELECT in UNION + when OFFSET/LIMIT is specifically not present. + + E.g. (SELECT ...) UNION (SELECT ..) + + This is known to fail on SQLite. It also fails on Oracle + because without LIMIT/OFFSET, there is currently no step that + creates an additional subquery. + + """ + return exclusions.open() + + @property + def offset(self): + """Target database must support some method of adding OFFSET or + equivalent to a result set.""" + return exclusions.open() + + @property + def sql_expression_limit_offset(self): + return exclusions.open() + + @property + def window_functions(self): + return exclusions.open() + + @property + def two_phase_transactions(self): + """Target database must support two-phase transactions.""" + + return exclusions.open() + + @property + def two_phase_recovery(self): + return exclusions.open() + + @property + def views(self): + """Target database must support VIEWs.""" + + return exclusions.open() + + @property + def empty_strings_varchar(self): + """ + target database can persist/return an empty string with a varchar. + """ + # opengauss treat empty string as NULL + return exclusions.closed() + + @property + def empty_strings_text(self): + """target database can persist/return an empty string with an + unbounded text.""" + # opengauss treat empty string as NULL + return exclusions.closed() + + @property + def empty_inserts_executemany(self): + return exclusions.open() + + @property + def expressions_against_unbounded_text(self): + """target database supports use of an unbounded textual field in a + WHERE clause.""" + + return exclusions.open() + + @property + def unicode_data(self): + """target drive must support unicode data stored in columns.""" + return exclusions.open() + + @property + def unicode_connections(self): + """ + Target driver must support some encoding of Unicode across the wire. + + """ + return exclusions.open() + + @property + def unicode_ddl(self): + """Target driver must support some degree of non-ascii symbol names.""" + + return exclusions.open() + + @property + def symbol_names_w_double_quote(self): + """Target driver can create tables with a name like 'some " table'""" + + return exclusions.open() + + @property + def emulated_lastrowid(self): + """ "target dialect retrieves cursor.lastrowid or an equivalent + after an insert() construct executes. + """ + return exclusions.closed() + + @property + def emulated_lastrowid_even_with_sequences(self): + """ "target dialect retrieves cursor.lastrowid or an equivalent + after an insert() construct executes, even if the table has a + Sequence on it. + """ + return exclusions.closed() + + @property + def implements_get_lastrowid(self): + return exclusions.open() + + @property + def dbapi_lastrowid(self): + """ "target backend includes a 'lastrowid' accessor on the DBAPI + cursor object. + + """ + return exclusions.closed() + + @property + def nullsordering(self): + """Target backends that support nulls ordering.""" + return exclusions.open() + + @property + def reflects_pk_names(self): + """Target driver reflects the name of primary key constraints.""" + + return exclusions.open() + + @property + def nested_aggregates(self): + """target database can select an aggregate from a subquery that's + also using an aggregate""" + + return exclusions.open() + + @property + def tuple_valued_builtin_functions(self): + return exclusions.open() + + @property + def array_type(self): + return exclusions.open() + + @property + def json_type(self): + return exclusions.closed() + + @property + def json_index_supplementary_unicode_element(self): + return exclusions.open() + + @property + def legacy_unconditional_json_extract(self): + """Backend has a JSON_EXTRACT or similar function that returns a + valid JSON string in all cases. + + Used to test a legacy feature and is not needed. + + """ + return exclusions.open() + + @property + def sqlite_memory(self): + return exclusions.closed() + + @property + def reflects_json_type(self): + return exclusions.closed() + + @property + def json_array_indexes(self): + return self.json_type + + @property + def datetime_literals(self): + """target dialect supports rendering of a date, time, or datetime as a + literal string, e.g. via the TypeEngine.literal_processor() method. + + """ + + return exclusions.closed() + + @property + def datetime(self): + """target dialect supports representation of Python + datetime.datetime() objects.""" + + return exclusions.open() + + @property + def datetime_microseconds(self): + """target dialect supports representation of Python + datetime.datetime() with microsecond objects.""" + + return exclusions.open() + + @property + def timestamp_microseconds(self): + """target dialect supports representation of Python + datetime.datetime() with microsecond objects but only + if TIMESTAMP is used.""" + + return exclusions.open() + + @property + def datetime_historic(self): + """target dialect supports representation of Python + datetime.datetime() objects with historic (pre 1900) values.""" + + return exclusions.open() + + @property + def date(self): + """target dialect supports representation of Python + datetime.date() objects.""" + + return exclusions.closed() + + @property + def date_coerces_from_datetime(self): + """target dialect accepts a datetime object as the target + of a date column.""" + + return exclusions.closed() + + @property + def date_historic(self): + """target dialect supports representation of Python + datetime.datetime() objects with historic (pre 1900) values.""" + + return exclusions.closed() + + @property + def time(self): + """target dialect supports representation of Python + datetime.time() objects.""" + + return exclusions.closed() + + @property + def time_microseconds(self): + """target dialect supports representation of Python + datetime.time() with microsecond objects.""" + + return exclusions.closed() + + @property + def precision_numerics_general(self): + """target backend has general support for moderately high-precision + numerics.""" + return exclusions.open() + + @property + def precision_numerics_enotation_small(self): + """target backend supports Decimal() objects using E notation + to represent very small values.""" + # NOTE: this exclusion isn't used in current tests. + return exclusions.open() + + @property + def precision_numerics_enotation_large(self): + """target backend supports Decimal() objects using E notation + to represent very large values.""" + + return exclusions.open() + + @property + def precision_numerics_many_significant_digits(self): + """target backend supports values with many digits on both sides, + such as 319438950232418390.273596, 87673.594069654243 + + """ + + return exclusions.open() + + @property + def cast_precision_numerics_many_significant_digits(self): + """same as precision_numerics_many_significant_digits but within the + context of a CAST statement (hello MySQL) + + """ + return exclusions.open() + + @property + def precision_numerics_retains_significant_digits(self): + """A precision numeric type will return empty significant digits, + i.e. a value such as 10.000 will come back in Decimal form with + the .000 maintained.""" + + return exclusions.open() + + @property + def infinity_floats(self): + return exclusions.open() + + @property + def precision_generic_float_type(self): + """target backend will return native floating point numbers with at + least seven decimal places when using the generic Float type.""" + + return exclusions.open() + + @property + def floats_to_four_decimals(self): + return exclusions.open() + + @property + def implicit_decimal_binds(self): + """target backend will return a selected Decimal as a Decimal, not + a string. + + e.g.:: + + expr = decimal.Decimal("15.7563") + + value = e.scalar( + select(literal(expr)) + ) + + assert value == expr + + See :ticket:`4036` + + """ + + return exclusions.open() + + @property + def fetch_null_from_numeric(self): + return exclusions.open() + + @property + def duplicate_key_raises_integrity_error(self): + return exclusions.open() + + @property + def hstore(self): + return exclusions.open() + + @property + def btree_gist(self): + return exclusions.open() + + @property + def range_types(self): + return exclusions.open() + + @property + def async_dialect(self): + """dialect makes use of await_() to invoke operations on the DBAPI.""" + + return exclusions.closed() + + @property + def postgresql_jsonb(self): + return exclusions.closed() + + @property + def psycopg2_native_hstore(self): + return self.psycopg2_compatibility + + @property + def psycopg2_compatibility(self): + return exclusions.open() + + @property + def psycopg2_or_pg8000_compatibility(self): + return exclusions.open() + + @property + def percent_schema_names(self): + return exclusions.open() + + @property + def order_by_label_with_expression(self): + return exclusions.closed() + + def get_order_by_collation(self, config): + return "POSIX" + + @property + def legacy_engine(self): + return exclusions.open() + + @property + def ad_hoc_engines(self): + return exclusions.open() + + @property + def no_asyncio(self): + + return exclusions.open() + + @property + def pyodbc_fast_executemany(self): + return exclusions.closed() + + @property + def python_fixed_issue_8743(self): + return exclusions.skip_if( + lambda: sys.version_info < (2, 7, 8), + "Python issue 8743 fixed in Python 2.7.8", + ) + + @property + def granular_timezone(self): + """the datetime.timezone class, or SQLAlchemy's port, supports + seconds and microseconds. + + SQLAlchemy ported the Python 3.7 version for Python 2, so + it passes on that. For Python 3.6 and earlier, it is not supported. + + """ + return exclusions.skip_if( + lambda: sys.version_info >= (3,) and sys.version_info < (3, 7) + ) + + @property + def selectone(self): + """target driver must support the literal statement 'select 1'""" + return exclusions.open() + + @property + def postgresql_utf8_server_encoding(self): + return exclusions.open() + + @property + def computed_columns(self): + return exclusions.closed() + + @property + def python_profiling_backend(self): + return exclusions.closed() + + @property + def computed_columns_stored(self): + return exclusions.open() + + @property + def computed_columns_virtual(self): + return exclusions.closed() + + @property + def computed_columns_default_persisted(self): + return exclusions.open() + + @property + def computed_columns_reflect_persisted(self): + return exclusions.open() + + @property + def regexp_match(self): + return exclusions.open() + + @property + def regexp_replace(self): + return exclusions.open() + + @property + def supports_distinct_on(self): + """If a backend supports the DISTINCT ON in a select""" + return exclusions.open() + + @property + def supports_for_update_of(self): + return exclusions.closed() + + @property + def sequences_in_other_clauses(self): + """sequences allowed in WHERE, GROUP BY, HAVING, etc.""" + return exclusions.open() + + @property + def supports_lastrowid_for_expressions(self): + """cursor.lastrowid works if an explicit SQL expression was used.""" + return exclusions.closed() + + @property + def supports_sequence_for_autoincrement_column(self): + """for mssql, autoincrement means IDENTITY, not sequence""" + return exclusions.open() + + @property + def identity_columns(self): + return exclusions.closed() + + @property + def identity_columns_standard(self): + return exclusions.closed() + + @property + def index_reflects_included_columns(self): + return exclusions.closed() + + @property + def fetch_first(self): + return exclusions.open() + + @property + def fetch_percent(self): + return exclusions.closed() + + @property + def fetch_ties(self): + return exclusions.closed() + + @property + def fetch_no_order_by(self): + return exclusions.open() + + @property + def fetch_offset_with_options(self): + # use together with fetch_first + return exclusions.open() + + @property + def fetch_expression(self): + # use together with fetch_first + return exclusions.open() + + @property + def autoincrement_without_sequence(self): + return exclusions.open() + + @property + def reflect_tables_no_columns(self): + # opengauss doesn't support this + return exclusions.closed() + + @property + def limit_with_strict_isotonicity(self): + """`LIMIT` in distributed opengauss does not have strict isotonicity. + + Use `limit` with `order_by` if you need strict isotonicity. + """ + return exclusions.only_on(["opengauss+psycopg2"]) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..aded715 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,16 @@ +[bdist_wheel] +universal=1 + +[tool:pytest] +addopts= --tb native -v -r fxX --maxfail=25 -p no:warnings +python_files=test/*test_*.py + +[sqla_testing] +requirement_cls=opengauss_sqlalchemy.requirements:Requirements +profile_file=test/profiles.txt + +[db] +default = opengauss://scott:Tiger123@127.0.0.1:37200/test +opengauss = opengauss+psycopg2://scott:Tiger123@127.0.0.1:37200/test +opengauss_psycopg2 = opengauss+psycopg2://scott:Tiger123@127.0.0.1:37200/test +opengauss_dc_psycopg2 = opengauss+dc_psycopg2://scott:Tiger123@127.0.0.1:37500/test diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..803c1ed --- /dev/null +++ b/setup.py @@ -0,0 +1,42 @@ +import os +import re + +from setuptools import setup + + +with open(os.path.join(os.path.dirname(__file__), "opengauss_sqlalchemy", "__init__.py")) as fd: + VERSION = re.compile(r'.*__version__ = "(.*?)"', re.S).match(fd.read()).group(1) + + +setup( + name="opengauss-sqlalchemy", + version=VERSION, + description="OpenGauss Dialect for SQLAlchemy", + author="Jia Junsu", + author_email="jiajunsu@huawei.com", + url="https://gitee.com/opengauss/openGauss-sqlalchemy", + license="MIT", + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Topic :: Database :: Front-Ends", + ], + packages=["opengauss_sqlalchemy"], + include_package_data=True, + install_requires=["SQLAlchemy<2.0", "psycopg2>=2.8"], + entry_points={ + "sqlalchemy.dialects": [ + "opengauss = opengauss_sqlalchemy.psycopg2:OpenGaussDialect_psycopg2", + "opengauss.psycopg2 = opengauss_sqlalchemy.psycopg2:OpenGaussDialect_psycopg2", + "opengauss.dc_psycopg2 = opengauss_sqlalchemy.dc_psycopg2:OpenGaussDialect_dc_psycopg2", + ] + }, +) diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 0000000..9d7492b --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,3 @@ +from sqlalchemy.testing.plugin.pytestplugin import * # noqa + +import opengauss_sqlalchemy # noqa diff --git a/test/test_compiler.py b/test/test_compiler.py new file mode 100644 index 0000000..35680ca --- /dev/null +++ b/test/test_compiler.py @@ -0,0 +1,646 @@ +from sqlalchemy import ( + and_, Column, exc, Float, func, Index, Integer, MetaData, Numeric, schema, String, Table, text +) +from sqlalchemy.testing import config +from sqlalchemy.testing import fixtures +from sqlalchemy.testing.assertions import assert_raises_message, AssertsCompiledSQL + +from opengauss_sqlalchemy import dc_psycopg2, psycopg2 + + +class DDLCompilerTest(fixtures.TestBase, AssertsCompiledSQL): + __dialect__ = psycopg2.dialect() + + def test_create_partial_index(self): + m = MetaData() + tbl = Table("testtbl", m, Column("data", Integer)) + idx = Index( + "test_idx1", + tbl.c.data, + opengauss_where=and_(tbl.c.data > 5, tbl.c.data < 10), + ) + idx = Index( + "test_idx1", + tbl.c.data, + opengauss_where=and_(tbl.c.data > 5, tbl.c.data < 10), + ) + + # test quoting and all that + + idx2 = Index( + "test_idx2", + tbl.c.data, + opengauss_where=and_(tbl.c.data > "a", tbl.c.data < "b's"), + ) + self.assert_compile( + schema.CreateIndex(idx), + "CREATE INDEX test_idx1 ON testtbl (data) " + "WHERE data > 5 AND data < 10", + ) + self.assert_compile( + schema.CreateIndex(idx2), + "CREATE INDEX test_idx2 ON testtbl (data) " + "WHERE data > 'a' AND data < 'b''s'", + ) + + idx3 = Index( + "test_idx2", + tbl.c.data, + opengauss_where=text("data > 'a' AND data < 'b''s'"), + ) + self.assert_compile( + schema.CreateIndex(idx3), + "CREATE INDEX test_idx2 ON testtbl (data) " + "WHERE data > 'a' AND data < 'b''s'", + ) + + def test_create_index_with_ops(self): + + m = MetaData() + tbl = Table( + "testtbl", + m, + Column("data", String), + Column("data2", Integer, key="d2"), + ) + + idx = Index( + "test_idx1", + tbl.c.data, + opengauss_ops={"data": "text_pattern_ops"}, + ) + + idx2 = Index( + "test_idx2", + tbl.c.data, + tbl.c.d2, + opengauss_ops={"data": "text_pattern_ops", "d2": "int4_ops"}, + ) + + self.assert_compile( + schema.CreateIndex(idx), + "CREATE INDEX test_idx1 ON testtbl " "(data text_pattern_ops)", + ) + self.assert_compile( + schema.CreateIndex(idx2), + "CREATE INDEX test_idx2 ON testtbl " + "(data text_pattern_ops, data2 int4_ops)", + ) + + def test_create_index_with_labeled_ops(self): + m = MetaData() + tbl = Table( + "testtbl", + m, + Column("data", String), + Column("data2", Integer, key="d2"), + ) + + idx = Index( + "test_idx1", + func.lower(tbl.c.data).label("data_lower"), + opengauss_ops={"data_lower": "text_pattern_ops"}, + ) + + idx2 = Index( + "test_idx2", + (func.xyz(tbl.c.data) + tbl.c.d2).label("bar"), + tbl.c.d2.label("foo"), + opengauss_ops={"bar": "text_pattern_ops", "foo": "int4_ops"}, + ) + + self.assert_compile( + schema.CreateIndex(idx), + "CREATE INDEX test_idx1 ON testtbl " + "(lower(data) text_pattern_ops)", + ) + self.assert_compile( + schema.CreateIndex(idx2), + "CREATE INDEX test_idx2 ON testtbl " + "((xyz(data) + data2) text_pattern_ops, " + "data2 int4_ops)", + ) + + def test_create_index_with_text_or_composite(self): + m = MetaData() + tbl = Table("testtbl", m, Column("d1", String), Column("d2", Integer)) + + idx = Index("test_idx1", text("x")) + tbl.append_constraint(idx) + + idx2 = Index("test_idx2", text("y"), tbl.c.d2) + + idx3 = Index( + "test_idx2", + tbl.c.d1, + text("y"), + tbl.c.d2, + opengauss_ops={"d1": "x1", "d2": "x2"}, + ) + + idx4 = Index( + "test_idx2", + tbl.c.d1, + tbl.c.d2 > 5, + text("q"), + opengauss_ops={"d1": "x1", "d2": "x2"}, + ) + + idx5 = Index( + "test_idx2", + tbl.c.d1, + (tbl.c.d2 > 5).label("g"), + text("q"), + opengauss_ops={"d1": "x1", "g": "x2"}, + ) + + self.assert_compile( + schema.CreateIndex(idx), "CREATE INDEX test_idx1 ON testtbl (x)" + ) + self.assert_compile( + schema.CreateIndex(idx2), + "CREATE INDEX test_idx2 ON testtbl (y, d2)", + ) + self.assert_compile( + schema.CreateIndex(idx3), + "CREATE INDEX test_idx2 ON testtbl (d1 x1, y, d2 x2)", + ) + + # note that at the moment we do not expect the 'd2' op to + # pick up on the "d2 > 5" expression + self.assert_compile( + schema.CreateIndex(idx4), + "CREATE INDEX test_idx2 ON testtbl (d1 x1, (d2 > 5), q)", + ) + + # however it does work if we label! + self.assert_compile( + schema.CreateIndex(idx5), + "CREATE INDEX test_idx2 ON testtbl (d1 x1, (d2 > 5) x2, q)", + ) + + def test_create_index_with_using(self): + m = MetaData() + tbl = Table("testtbl", m, Column("data", String)) + + idx1 = Index("test_idx1", tbl.c.data) + idx2 = Index("test_idx2", tbl.c.data, opengauss_using="btree") + idx3 = Index("test_idx3", tbl.c.data, opengauss_using="hash") + + self.assert_compile( + schema.CreateIndex(idx1), + "CREATE INDEX test_idx1 ON testtbl " "(data)", + ) + self.assert_compile( + schema.CreateIndex(idx2), + "CREATE INDEX test_idx2 ON testtbl " "USING btree (data)", + ) + self.assert_compile( + schema.CreateIndex(idx3), + "CREATE INDEX test_idx3 ON testtbl " "USING hash (data)", + ) + + def test_create_index_with_local_no_partition(self): + m = MetaData() + tbl = Table( + "testtbl", + m, + Column("data", Integer), + opengauss_partition_by="RANGE (data)" + ) + idx1 = Index("test_idx1", tbl.c.data) + idx2 = Index("test_idx2", tbl.c.data, opengauss_local=[""]) + + self.assert_compile( + schema.CreateIndex(idx1), + "CREATE INDEX test_idx1 ON testtbl (data)", + ) + self.assert_compile( + schema.CreateIndex(idx2), + "CREATE INDEX test_idx2 ON testtbl (data) LOCAL", + ) + + def test_create_index_with_local_partitions(self): + m = MetaData() + tbl = Table( + "testtbl", + m, + Column("data", Integer), + opengauss_partition_by="RANGE (data)" + ) + idx1 = Index( + "test_idx1", + tbl.c.data, + opengauss_local=[ + "PARTITION data_index1", + "PARTITION data_index2 TABLESPACE example3", + ] + ) + self.assert_compile( + schema.CreateIndex(idx1), + "CREATE INDEX test_idx1 ON testtbl (data) LOCAL " + "(PARTITION data_index1, PARTITION data_index2 TABLESPACE example3)", + ) + + def test_create_index_with_with(self): + m = MetaData() + tbl = Table("testtbl", m, Column("data", String)) + + idx1 = Index("test_idx1", tbl.c.data) + idx2 = Index( + "test_idx2", tbl.c.data, opengauss_with={"fillfactor": 50} + ) + idx3 = Index( + "test_idx3", + tbl.c.data, + opengauss_using="gist", + opengauss_with={"buffering": "off"}, + ) + + self.assert_compile( + schema.CreateIndex(idx1), + "CREATE INDEX test_idx1 ON testtbl " "(data)", + ) + self.assert_compile( + schema.CreateIndex(idx2), + "CREATE INDEX test_idx2 ON testtbl " + "(data) " + "WITH (fillfactor = 50)", + ) + self.assert_compile( + schema.CreateIndex(idx3), + "CREATE INDEX test_idx3 ON testtbl " + "USING gist (data) " + "WITH (buffering = off)", + ) + + def test_create_index_with_using_unusual_conditions(self): + m = MetaData() + tbl = Table("testtbl", m, Column("data", String)) + + self.assert_compile( + schema.CreateIndex( + Index("test_idx1", tbl.c.data, opengauss_using="GIST") + ), + "CREATE INDEX test_idx1 ON testtbl " "USING gist (data)", + ) + + self.assert_compile( + schema.CreateIndex( + Index( + "test_idx1", + tbl.c.data, + opengauss_using="some_custom_method", + ) + ), + "CREATE INDEX test_idx1 ON testtbl " + "USING some_custom_method (data)", + ) + + assert_raises_message( + exc.CompileError, + "Unexpected SQL phrase: 'gin invalid sql'", + schema.CreateIndex( + Index( + "test_idx2", tbl.c.data, opengauss_using="gin invalid sql" + ) + ).compile, + dialect=psycopg2.dialect(), + ) + + def test_create_index_with_tablespace(self): + m = MetaData() + tbl = Table("testtbl", m, Column("data", String)) + + idx1 = Index("test_idx1", tbl.c.data) + idx2 = Index( + "test_idx2", tbl.c.data, opengauss_tablespace="sometablespace" + ) + idx3 = Index( + "test_idx3", + tbl.c.data, + opengauss_tablespace="another table space", + ) + + self.assert_compile( + schema.CreateIndex(idx1), + "CREATE INDEX test_idx1 ON testtbl " "(data)", + ) + self.assert_compile( + schema.CreateIndex(idx2), + "CREATE INDEX test_idx2 ON testtbl " + "(data) " + "TABLESPACE sometablespace", + ) + self.assert_compile( + schema.CreateIndex(idx3), + "CREATE INDEX test_idx3 ON testtbl " + "(data) " + 'TABLESPACE "another table space"', + ) + + def test_create_index_with_multiple_options(self): + m = MetaData() + tbl = Table("testtbl", m, Column("data", String)) + + idx1 = Index( + "test_idx1", + tbl.c.data, + opengauss_using="btree", + opengauss_tablespace="atablespace", + opengauss_with={"fillfactor": 60}, + opengauss_where=and_(tbl.c.data > 5, tbl.c.data < 10), + ) + + self.assert_compile( + schema.CreateIndex(idx1), + "CREATE INDEX test_idx1 ON testtbl " + "USING btree (data) " + "WITH (fillfactor = 60) " + "TABLESPACE atablespace " + "WHERE data > 5 AND data < 10", + ) + + def test_create_index_expr_gets_parens(self): + m = MetaData() + tbl = Table("testtbl", m, Column("x", Integer), Column("y", Integer)) + + idx1 = Index("test_idx1", 5 / (tbl.c.x + tbl.c.y)) + self.assert_compile( + schema.CreateIndex(idx1), + "CREATE INDEX test_idx1 ON testtbl ((5 / (x + y)))", + ) + + def test_create_index_literals(self): + m = MetaData() + tbl = Table("testtbl", m, Column("data", Integer)) + + idx1 = Index("test_idx1", tbl.c.data + 5) + self.assert_compile( + schema.CreateIndex(idx1), + "CREATE INDEX test_idx1 ON testtbl ((data + 5))", + ) + + def test_create_index_concurrently(self): + m = MetaData() + tbl = Table("testtbl", m, Column("data", Integer)) + + idx1 = Index("test_idx1", tbl.c.data, opengauss_concurrently=True) + self.assert_compile( + schema.CreateIndex(idx1), + "CREATE INDEX CONCURRENTLY test_idx1 ON testtbl (data)", + ) + + def test_drop_index_concurrently(self): + m = MetaData() + tbl = Table("testtbl", m, Column("data", Integer)) + + idx1 = Index("test_idx1", tbl.c.data, opengauss_concurrently=True) + self.assert_compile( + schema.DropIndex(idx1), "DROP INDEX CONCURRENTLY test_idx1" + ) + + def test_create_table_with_with_clause(self): + m = MetaData() + tbl = Table( + "atable", + m, + Column("id", Integer), + opengauss_with={"ORIENTATION": "COLUMN"}, + ) + self.assert_compile( + schema.CreateTable(tbl), + "CREATE TABLE atable (id INTEGER) WITH (ORIENTATION = COLUMN)", + ) + + def test_create_table_with_tablespace(self): + m = MetaData() + tbl = Table( + "atable", + m, + Column("id", Integer), + opengauss_tablespace="sometablespace", + ) + self.assert_compile( + schema.CreateTable(tbl), + "CREATE TABLE atable (id INTEGER) TABLESPACE sometablespace", + ) + + def test_create_table_with_tablespace_quoted(self): + # testing quoting of tablespace name + m = MetaData() + tbl = Table( + "anothertable", + m, + Column("id", Integer), + opengauss_tablespace="table", + ) + self.assert_compile( + schema.CreateTable(tbl), + 'CREATE TABLE anothertable (id INTEGER) TABLESPACE "table"', + ) + + def test_create_table_with_oncommit_option(self): + m = MetaData() + tbl = Table( + "atable", + m, + Column("id", Integer), + prefixes=["TEMPORARY"], + opengauss_on_commit="DROP", + ) + self.assert_compile( + schema.CreateTable(tbl), + "CREATE TEMPORARY TABLE atable (id INTEGER) ON COMMIT DROP", + ) + + def test_create_table_with_compress(self): + m = MetaData() + tbl = Table( + "atable", + m, + Column("id", Integer), + opengauss_with={"ORIENTATION": "COLUMN"}, + opengauss_compress=True, + ) + self.assert_compile( + schema.CreateTable(tbl), + "CREATE TABLE atable (id INTEGER) WITH (ORIENTATION = COLUMN) COMPRESS", + ) + + def test_create_table_partition_by_list(self): + m = MetaData() + tbl = Table( + "atable", + m, + Column("id", Integer), + Column("part_column", Integer), + opengauss_partition_by="LIST (part_column)", + ) + self.assert_compile( + schema.CreateTable(tbl), + "CREATE TABLE atable (id INTEGER, part_column INTEGER) " + "PARTITION BY LIST (part_column)", + ) + + def test_create_table_partition_by_range(self): + m = MetaData() + tbl = Table( + "atable", + m, + Column("id", Integer), + Column("part_column", Integer), + opengauss_partition_by="RANGE (part_column)", + ) + self.assert_compile( + schema.CreateTable(tbl), + "CREATE TABLE atable (id INTEGER, part_column INTEGER) " + "PARTITION BY RANGE (part_column)", + ) + + def test_create_table_enable_row_movement(self): + m = MetaData() + tbl = Table( + "atable", + m, + Column("id", Integer), + Column("part_column", Integer), + opengauss_partition_by="RANGE (part_column)", + opengauss_enable_row_movement=True, + ) + self.assert_compile( + schema.CreateTable(tbl), + "CREATE TABLE atable (id INTEGER, part_column INTEGER) " + "PARTITION BY RANGE (part_column) " + "ENABLE ROW MOVEMENT", + ) + + +class DDLCompilerTest_dc_psycopg2(DDLCompilerTest): + __dialect__ = dc_psycopg2.dialect() + + def test_create_index_concurrently(self): + config.skip_test("Distribited mode unsupport create index concurrently") + + def test_drop_index_concurrently(self): + config.skip_test("Distribited mode unsupport drop index concurrently") + + def test_create_table_distribute_by_replication(self): + m = MetaData() + tbl = Table( + "atable", + m, + Column("id", Integer), + opengauss_distribute_by="REPLICATION", + ) + self.assert_compile( + schema.CreateTable(tbl), + "CREATE TABLE atable (id INTEGER) " + "DISTRIBUTE BY REPLICATION", + ) + + def test_create_table_distribute_by_hash(self): + m = MetaData() + tbl = Table( + "atable", + m, + Column("id", Integer), + opengauss_distribute_by="HASH(id)", + ) + self.assert_compile( + schema.CreateTable(tbl), + "CREATE TABLE atable (id INTEGER) " + "DISTRIBUTE BY HASH(id)", + ) + + def test_create_table_distribute_by_range(self): + m = MetaData() + tbl = Table( + "atable", + m, + Column("id", Integer), + Column("distri_col", Integer), + opengauss_distribute_by="RANGE(distri_col) (SLICE s1 VALUES LESS THAN(10), " + "SLICE s2 VALUES LESS THAN (MAXVALUE))", + ) + self.assert_compile( + schema.CreateTable(tbl), + "CREATE TABLE atable (id INTEGER, distri_col INTEGER) " + "DISTRIBUTE BY RANGE(distri_col) (SLICE s1 VALUES LESS THAN(10), SLICE s2 VALUES LESS THAN (MAXVALUE))", + ) + + def test_create_table_distribute_by_list(self): + m = MetaData() + tbl = Table( + "atable", + m, + Column("id", Integer), + Column("distri_col", String(16)), + opengauss_distribute_by="LIST(distri_col) (SLICE s1 VALUES ('D1'), " + "SLICE s2 VALUES (DEFAULT))", + ) + self.assert_compile( + schema.CreateTable(tbl), + "CREATE TABLE atable (id INTEGER, distri_col VARCHAR(16)) " + "DISTRIBUTE BY LIST(distri_col) (SLICE s1 VALUES ('D1'), SLICE s2 VALUES (DEFAULT))", + ) + + def test_create_table_without_distributable_column_float(self): + m = MetaData() + tbl = Table( + "atable", + m, + Column("undistributable_col", Float), + ) + self.assert_compile( + schema.CreateTable(tbl), + "CREATE TABLE atable (undistributable_col FLOAT) " + "DISTRIBUTE BY REPLICATION", + ) + + def test_create_table_without_distributable_column_float_as_decimal(self): + m = MetaData() + tbl = Table( + "atable", + m, + Column("undistributable_col", Float(precision=8, asdecimal=True)), + ) + self.assert_compile( + schema.CreateTable(tbl), + "CREATE TABLE atable (undistributable_col FLOAT(8)) " + "DISTRIBUTE BY REPLICATION", + ) + + def test_create_table_with_distributable_column_numeric(self): + m = MetaData() + tbl = Table( + "atable", + m, + Column("distributable_col", Numeric(18, 14)), + ) + self.assert_compile( + schema.CreateTable(tbl), + "CREATE TABLE atable (distributable_col NUMERIC(18, 14))", + ) + + def test_create_table_with_distributable_column_varchar(self): + m = MetaData() + tbl = Table( + "atable", + m, + Column("distributable_col", String(40)), + ) + self.assert_compile( + schema.CreateTable(tbl), + "CREATE TABLE atable (distributable_col VARCHAR(40))", + ) + + def test_create_table_with_to_group(self): + m = MetaData() + tbl = Table( + "atable", m, Column("id", Integer), opengauss_to="GROUP group_s1", + ) + self.assert_compile( + schema.CreateTable(tbl), + "CREATE TABLE atable (id INTEGER) TO GROUP group_s1", + ) diff --git a/test/test_suite.py b/test/test_suite.py new file mode 100644 index 0000000..812b44a --- /dev/null +++ b/test/test_suite.py @@ -0,0 +1,872 @@ +# coding: utf-8 +import operator + +import sqlalchemy as sa +from sqlalchemy.schema import DDL, Index +from sqlalchemy import inspect, testing +from sqlalchemy import types as sql_types +from sqlalchemy.testing.provision import get_temp_table_name, temp_table_keyword_args +from sqlalchemy.testing.suite import * # noqa +from sqlalchemy.testing.suite import ComponentReflectionTestExtra as _ComponentReflectionTestExtra +from sqlalchemy.testing.suite.test_cte import CTETest as _CTETest +from sqlalchemy.testing.suite.test_ddl import LongNameBlowoutTest as _LongNameBlowoutTest +from sqlalchemy.testing.suite.test_reflection import ComponentReflectionTest as _ComponentReflectionTest +from sqlalchemy.testing.suite.test_reflection import CompositeKeyReflectionTest as _CompositeKeyReflectionTest +from sqlalchemy.testing.suite.test_reflection import QuotedNameArgumentTest as _QuotedNameArgumentTest +from sqlalchemy.testing.suite.test_results import ServerSideCursorsTest as _ServerSideCursorsTest +from sqlalchemy.testing.suite.test_select import FetchLimitOffsetTest as _FetchLimitOffsetTest +from sqlalchemy.testing.suite.test_select import JoinTest as _JoinTest +from sqlalchemy.testing.suite.test_unicode_ddl import UnicodeSchemaTest as _UnicodeSchemaTest + + +class CTETest(_CTETest): + @classmethod + def define_tables(cls, metadata): + if testing.requires.foreign_keys.enabled: + Table( + "some_table", + metadata, + Column("id", Integer, primary_key=True), + Column("data", String(50)), + Column("parent_id", ForeignKey("some_table.id")), + ) + else: + Table( + "some_table", + metadata, + Column("id", Integer, primary_key=True), + Column("data", String(50)), + Column("parent_id", Integer), + ) + + Table( + "some_other_table", + metadata, + Column("id", Integer, primary_key=True), + Column("data", String(50)), + Column("parent_id", Integer), + ) + + +class LongNameBlowoutTest(_LongNameBlowoutTest): + @testing.combinations( + ("fk", testing.requires.foreign_keys), + ("pk",), + ("ix",), + ("ck", testing.requires.check_constraint_reflection.as_skips()), + ("uq", testing.requires.unique_constraint_reflection.as_skips()), + argnames="type_", + ) + def test_long_convention_name(self, type_, metadata, connection): + actual_name, reflected_name = getattr(self, type_)( + metadata, connection + ) + + assert len(actual_name) > 255 + + if reflected_name is not None: + overlap = actual_name[0 : len(reflected_name)] + if len(overlap) < len(actual_name): + eq_(overlap[0:-5], reflected_name[0 : len(overlap) - 5]) + else: + eq_(overlap, reflected_name) + + +class ComponentReflectionTest(_ComponentReflectionTest): + @classmethod + def define_reflected_tables(cls, metadata, schema): + if schema: + schema_prefix = schema + "." + else: + schema_prefix = "" + + if testing.requires.self_referential_foreign_keys.enabled: + users = Table( + "users", + metadata, + Column("user_id", sa.INT, primary_key=True), + Column("test1", sa.CHAR(5), nullable=False), + Column("test2", sa.Float(5), nullable=False), + Column( + "parent_user_id", + sa.Integer, + sa.ForeignKey( + "%susers.user_id" % schema_prefix, name="user_id_fk" + ), + ), + schema=schema, + test_needs_fk=True, + ) + else: + users = Table( + "users", + metadata, + Column("user_id", sa.INT, primary_key=True), + Column("test1", sa.CHAR(5), nullable=False), + Column("test2", sa.Float(5), nullable=False), + schema=schema, + test_needs_fk=True, + ) + + if testing.requires.foreign_keys.enabled: + # distributed opengauss does NOT support foreign keys + Table( + "dingalings", + metadata, + Column("dingaling_id", sa.Integer, primary_key=True), + Column( + "address_id", + sa.Integer, + sa.ForeignKey("%semail_addresses.address_id" % schema_prefix), + ), + Column("data", sa.String(30)), + schema=schema, + test_needs_fk=True, + ) + Table( + "email_addresses", + metadata, + Column("address_id", sa.Integer), + Column( + "remote_user_id", sa.Integer, sa.ForeignKey(users.c.user_id) + ), + Column("email_address", sa.String(20)), + sa.PrimaryKeyConstraint("address_id", name="email_ad_pk"), + schema=schema, + test_needs_fk=True, + ) + else: + Table( + "dingalings", + metadata, + Column("dingaling_id", sa.Integer, primary_key=True), + Column( + "address_id", + sa.Integer, + ), + Column("data", sa.String(30)), + schema=schema, + test_needs_fk=True, + ) + Table( + "email_addresses", + metadata, + Column("address_id", sa.Integer), + Column( + "remote_user_id", sa.Integer + ), + Column("email_address", sa.String(20)), + sa.PrimaryKeyConstraint("address_id", name="email_ad_pk"), + schema=schema, + test_needs_fk=True, + ) + Table( + "comment_test", + metadata, + Column("id", sa.Integer, primary_key=True, comment="id comment"), + Column("data", sa.String(20), comment="data % comment"), + Column( + "d2", + sa.String(20), + comment=r"""Comment types type speedily ' " \ '' Fun!""", + ), + schema=schema, + comment=r"""the test % ' " \ table comment""", + ) + + if testing.requires.cross_schema_fk_reflection.enabled: + if schema is None: + Table( + "local_table", + metadata, + Column("id", sa.Integer, primary_key=True), + Column("data", sa.String(20)), + Column( + "remote_id", + ForeignKey( + "%s.remote_table_2.id" % testing.config.test_schema + ), + ), + test_needs_fk=True, + schema=config.db.dialect.default_schema_name, + ) + else: + Table( + "remote_table", + metadata, + Column("id", sa.Integer, primary_key=True), + Column( + "local_id", + ForeignKey( + "%s.local_table.id" + % config.db.dialect.default_schema_name + ), + ), + Column("data", sa.String(20)), + schema=schema, + test_needs_fk=True, + ) + Table( + "remote_table_2", + metadata, + Column("id", sa.Integer, primary_key=True), + Column("data", sa.String(20)), + schema=schema, + test_needs_fk=True, + ) + + if testing.requires.index_reflection.enabled: + cls.define_index(metadata, users) + + if not schema: + # test_needs_fk is at the moment to force MySQL InnoDB + noncol_idx_test_nopk = Table( + "noncol_idx_test_nopk", + metadata, + Column("q", sa.String(5)), + test_needs_fk=True, + ) + + noncol_idx_test_pk = Table( + "noncol_idx_test_pk", + metadata, + Column("id", sa.Integer, primary_key=True), + Column("q", sa.String(5)), + test_needs_fk=True, + ) + + if testing.requires.indexes_with_ascdesc.enabled: + Index("noncol_idx_nopk", noncol_idx_test_nopk.c.q.desc()) + Index("noncol_idx_pk", noncol_idx_test_pk.c.q.desc()) + + if testing.requires.view_column_reflection.enabled: + cls.define_views(metadata, schema) + if not schema and testing.requires.temp_table_reflection.enabled: + cls.define_temp_tables(metadata) + + @classmethod + def define_temp_tables(cls, metadata): + kw = temp_table_keyword_args(config, config.db) + table_name = get_temp_table_name( + config, config.db, "user_tmp_%s" % config.ident + ) + user_tmp = Table( + table_name, + metadata, + # local temp table does NOT support serial column in opengauss + Column("id", sa.INT), + Column("name", sa.VARCHAR(50)), + Column("foo", sa.INT), + # disambiguate temp table unique constraint names. this is + # pretty arbitrary for a generic dialect however we are doing + # it to suit SQL Server which will produce name conflicts for + # unique constraints created against temp tables in different + # databases. + # https://www.arbinada.com/en/node/1645 + sa.UniqueConstraint("name", name="user_tmp_uq_%s" % config.ident), + sa.Index("user_tmp_ix", "foo"), + **kw + ) + if ( + testing.requires.view_reflection.enabled + and testing.requires.temporary_views.enabled + ): + event.listen( + user_tmp, + "after_create", + DDL( + "create temporary view user_tmp_v as " + "select * from user_tmp_%s" % config.ident + ), + ) + event.listen(user_tmp, "before_drop", DDL("drop view user_tmp_v")) + + @testing.combinations( + (True, testing.requires.schemas), (False,), argnames="use_schema" + ) + @testing.requires.unique_constraint_reflection + def test_get_unique_constraints(self, metadata, connection, use_schema): + if use_schema: + schema = config.test_schema + else: + schema = None + uniques = sorted( + [ + {"name": "unique_a", "column_names": ["a"]}, + {"name": "unique_a_b_c", "column_names": ["a", "b", "c"]}, + {"name": "unique_c_a_b", "column_names": ["c", "a", "b"]}, + {"name": "unique_asc_key", "column_names": ["asc", "key"]}, + {"name": "i.have.dots", "column_names": ["b"]}, + {"name": "i have spaces", "column_names": ["c"]}, + ], + key=operator.itemgetter("name"), + ) + + if testing.against("opengauss+dc_psycopg2"): + table = Table( + "testtbl", + metadata, + Column("a", sa.String(20)), + Column("b", sa.String(30)), + Column("c", sa.Integer), + # reserved identifiers + Column("asc", sa.String(30)), + Column("key", sa.String(30)), + schema=schema, + opengauss_distribute_by='REPLICATION', + ) + else: + table = Table( + "testtbl", + metadata, + Column("a", sa.String(20)), + Column("b", sa.String(30)), + Column("c", sa.Integer), + # reserved identifiers + Column("asc", sa.String(30)), + Column("key", sa.String(30)), + schema=schema, + ) + for uc in uniques: + table.append_constraint( + sa.UniqueConstraint(*uc["column_names"], name=uc["name"]) + ) + table.create(connection) + + inspector = inspect(connection) + reflected = sorted( + inspector.get_unique_constraints("testtbl", schema=schema), + key=operator.itemgetter("name"), + ) + + names_that_duplicate_index = set() + + for orig, refl in zip(uniques, reflected): + # Different dialects handle duplicate index and constraints + # differently, so ignore this flag + dupe = refl.pop("duplicates_index", None) + if dupe: + names_that_duplicate_index.add(dupe) + eq_(orig, refl) + + reflected_metadata = MetaData() + reflected = Table( + "testtbl", + reflected_metadata, + autoload_with=connection, + schema=schema, + ) + + # test "deduplicates for index" logic. MySQL and Oracle + # "unique constraints" are actually unique indexes (with possible + # exception of a unique that is a dupe of another one in the case + # of Oracle). make sure # they aren't duplicated. + idx_names = set([idx.name for idx in reflected.indexes]) + uq_names = set( + [ + uq.name + for uq in reflected.constraints + if isinstance(uq, sa.UniqueConstraint) + ] + ).difference(["unique_c_a_b"]) + + assert not idx_names.intersection(uq_names) + if names_that_duplicate_index: + eq_(names_that_duplicate_index, idx_names) + eq_(uq_names, set()) + + +class CompositeKeyReflectionTest(_CompositeKeyReflectionTest): + @classmethod + def define_tables(cls, metadata): + tb1 = Table( + "tb1", + metadata, + Column("id", Integer), + Column("attr", Integer), + Column("name", sql_types.VARCHAR(20)), + sa.PrimaryKeyConstraint("name", "id", "attr", name="pk_tb1"), + schema=None, + test_needs_fk=True, + ) + if testing.requires.foreign_key_constraint_reflection.enabled: + Table( + "tb2", + metadata, + Column("id", Integer, primary_key=True), + Column("pid", Integer), + Column("pattr", Integer), + Column("pname", sql_types.VARCHAR(20)), + sa.ForeignKeyConstraint( + ["pname", "pid", "pattr"], + [tb1.c.name, tb1.c.id, tb1.c.attr], + name="fk_tb1_name_id_attr", + ), + schema=None, + test_needs_fk=True, + ) + + +class ComponentReflectionTestExtra(_ComponentReflectionTestExtra): + @testing.combinations( + ( + None, + "CASCADE", + None, + testing.requires.foreign_key_constraint_option_reflection_ondelete, + ), + ( + None, + None, + "SET NULL", + testing.requires.foreign_key_constraint_option_reflection_onupdate, + ), + ( + {}, + None, + "NO ACTION", + testing.requires.foreign_key_constraint_option_reflection_onupdate, + ), + ( + {}, + "NO ACTION", + None, + testing.requires.fk_constraint_option_reflection_ondelete_noaction, + ), + ( + None, + None, + "RESTRICT", + testing.requires.fk_constraint_option_reflection_onupdate_restrict, + ), + ( + None, + "RESTRICT", + None, + testing.requires.fk_constraint_option_reflection_ondelete_restrict, + ), + argnames="expected,ondelete,onupdate", + ) + def test_get_foreign_key_options( + self, connection, metadata, expected, ondelete, onupdate + ): + options = {} + if ondelete: + options["ondelete"] = ondelete + if onupdate: + options["onupdate"] = onupdate + + if expected is None: + expected = options + + Table( + "x", + metadata, + Column("id", Integer, primary_key=True), + test_needs_fk=True, + ) + + Table( + "table", + metadata, + Column("id", Integer, primary_key=True), + Column("x_id", Integer, sa.ForeignKey("x.id", name="xid")), + Column("test", String(10)), + test_needs_fk=True, + ) + + # tid is system column name in opengauss, change it to "tid_" + Table( + "user", + metadata, + Column("id", Integer, primary_key=True), + Column("name", String(50), nullable=False), + Column("tid_", Integer), + sa.ForeignKeyConstraint( + ["tid_"], ["table.id"], name="myfk", **options + ), + test_needs_fk=True, + ) + + metadata.create_all(connection) + + insp = inspect(connection) + + # test 'options' is always present for a backend + # that can reflect these, since alembic looks for this + opts = insp.get_foreign_keys("table")[0]["options"] + + eq_(dict((k, opts[k]) for k in opts if opts[k]), {}) + + opts = insp.get_foreign_keys("user")[0]["options"] + eq_(opts, expected) + + +class QuotedNameArgumentTest(_QuotedNameArgumentTest): + @classmethod + def define_tables(cls, metadata): + if testing.requires.foreign_keys.enabled: + Table( + "quote ' one", + metadata, + Column("id", Integer), + Column("name", String(50)), + Column("data", String(50)), + Column("related_id", Integer), + sa.PrimaryKeyConstraint("id", name="pk quote ' one"), + sa.Index("ix quote ' one", "name"), + sa.UniqueConstraint( + "data", + name="uq quote' one", + ), + sa.ForeignKeyConstraint( + ["id"], ["related.id"], name="fk quote ' one" + ), + sa.CheckConstraint("name != 'foo'", name="ck quote ' one"), + comment=r"""quote ' one comment""", + test_needs_fk=True, + ) + + if testing.requires.symbol_names_w_double_quote.enabled: + Table( + 'quote " two', + metadata, + Column("id", Integer), + Column("name", String(50)), + Column("data", String(50)), + Column("related_id", Integer), + sa.PrimaryKeyConstraint("id", name='pk quote " two'), + sa.Index('ix quote " two', "name"), + sa.UniqueConstraint( + "data", + name='uq quote" two', + ), + sa.ForeignKeyConstraint( + ["id"], ["related.id"], name='fk quote " two' + ), + sa.CheckConstraint("name != 'foo'", name='ck quote " two '), + comment=r"""quote " two comment""", + test_needs_fk=True, + ) + else: + Table( + "quote ' one", + metadata, + Column("id", Integer), + Column("name", String(50)), + Column("data", String(50)), + Column("related_id", Integer), + sa.PrimaryKeyConstraint("id", name="pk quote ' one"), + sa.Index("ix quote ' one", "name"), + sa.UniqueConstraint( + "data", + name="uq quote' one", + ), + sa.CheckConstraint("name != 'foo'", name="ck quote ' one"), + comment=r"""quote ' one comment""", + test_needs_fk=True, + opengauss_distribute_by='REPLICATION', + ) + + if testing.requires.symbol_names_w_double_quote.enabled: + Table( + 'quote " two', + metadata, + Column("id", Integer), + Column("name", String(50)), + Column("data", String(50)), + Column("related_id", Integer), + sa.PrimaryKeyConstraint("id", name='pk quote " two'), + sa.Index('ix quote " two', "name"), + sa.UniqueConstraint( + "data", + name='uq quote" two', + ), + sa.CheckConstraint("name != 'foo'", name='ck quote " two '), + comment=r"""quote " two comment""", + test_needs_fk=True, + opengauss_distribute_by='REPLICATION', + ) + + Table( + "related", + metadata, + Column("id", Integer, primary_key=True), + Column("related", Integer), + test_needs_fk=True, + ) + + if testing.requires.view_column_reflection.enabled: + + if testing.requires.symbol_names_w_double_quote.enabled: + names = [ + "quote ' one", + 'quote " two', + ] + else: + names = [ + "quote ' one", + ] + for name in names: + query = "CREATE VIEW %s AS SELECT * FROM %s" % ( + config.db.dialect.identifier_preparer.quote( + "view %s" % name + ), + config.db.dialect.identifier_preparer.quote(name), + ) + + event.listen(metadata, "after_create", DDL(query)) + event.listen( + metadata, + "before_drop", + DDL( + "DROP VIEW %s" + % config.db.dialect.identifier_preparer.quote( + "view %s" % name + ) + ), + ) + + def quote_fixtures(fn): + return testing.combinations( + ("quote ' one",), + ('quote " two', testing.requires.symbol_names_w_double_quote), + )(fn) + + @quote_fixtures + @testing.requires.foreign_keys + def test_get_foreign_keys(self, name): + insp = inspect(config.db) + assert insp.get_foreign_keys(name) + + +class ServerSideCursorsTest(_ServerSideCursorsTest): + def _is_server_side(self, cursor): + # TODO: this is a huge issue as it prevents these tests from being + # usable by third party dialects. + if self.engine.dialect.driver in ("psycopg2", "dc_psycopg2"): + return bool(cursor.name) + elif self.engine.dialect.driver == "pymysql": + sscursor = __import__("pymysql.cursors").cursors.SSCursor + return isinstance(cursor, sscursor) + elif self.engine.dialect.driver in ("aiomysql", "asyncmy"): + return cursor.server_side + elif self.engine.dialect.driver == "mysqldb": + sscursor = __import__("MySQLdb.cursors").cursors.SSCursor + return isinstance(cursor, sscursor) + elif self.engine.dialect.driver == "mariadbconnector": + return not cursor.buffered + elif self.engine.dialect.driver in ("asyncpg", "aiosqlite"): + return cursor.server_side + elif self.engine.dialect.driver == "pg8000": + return getattr(cursor, "server_side", False) + else: + return False + + +class FetchLimitOffsetTest(_FetchLimitOffsetTest): + def test_limit_render_multiple_times(self, connection): + table = self.tables.some_table + + if testing.requires.limit_with_strict_isotonicity.enabled: + stmt = select(table.c.id).limit(1).scalar_subquery() + else: + # LIMIT in distributed opengauss does not have strict isotonicity, + # so use it with `order_by` if you need strict isotonicity. + stmt = select(table.c.id).order_by(table.c.id).limit(1).scalar_subquery() + + u = union(select(stmt), select(stmt)).subquery().select() + + self._assert_result( + connection, + u, + [ + (1,), + ], + ) + + +class JoinTest(_JoinTest): + @classmethod + def define_tables(cls, metadata): + Table("a", metadata, Column("id", Integer, primary_key=True)) + + if testing.requires.foreign_keys.enabled: + # distributed opengauss does NOT support foreign keys + Table( + "b", + metadata, + Column("id", Integer, primary_key=True), + Column("a_id", ForeignKey("a.id"), nullable=False), + ) + else: + Table( + "b", + metadata, + Column("id", Integer, primary_key=True), + Column("a_id", Integer, nullable=False), + ) + + @testing.requires.foreign_keys + def test_inner_join_fk(self): + a, b = self.tables("a", "b") + + stmt = select(a, b).select_from(a.join(b)).order_by(a.c.id, b.c.id) + + self._assert_result(stmt, [(1, 1, 1), (1, 2, 1), (2, 4, 2), (3, 5, 3)]) + + @testing.requires.foreign_keys + def test_outer_join_fk(self): + a, b = self.tables("a", "b") + + stmt = select(a, b).select_from(a.join(b)).order_by(a.c.id, b.c.id) + + self._assert_result(stmt, [(1, 1, 1), (1, 2, 1), (2, 4, 2), (3, 5, 3)]) + + +class UnicodeSchemaTest(_UnicodeSchemaTest): + @classmethod + def define_tables(cls, metadata): + global t1, t2, t3 + + t1 = Table( + u("unitable1"), + metadata, + Column(u("méil"), Integer, primary_key=True), + Column(ue("\u6e2c\u8a66"), Integer), + test_needs_fk=True, + ) + if testing.requires.foreign_keys.enabled: + t2 = Table( + u("Unitéble2"), + metadata, + Column(u("méil"), Integer, primary_key=True, key="a"), + Column( + ue("\u6e2c\u8a66"), + Integer, + ForeignKey(u("unitable1.méil")), + key="b", + ), + test_needs_fk=True, + ) + + else: + t2 = Table( + u("Unitéble2"), + metadata, + Column(u("méil"), Integer, primary_key=True, key="a"), + Column( + ue("\u6e2c\u8a66"), + Integer, + key="b", + ), + test_needs_fk=True, + ) + + t3 = Table( + ue("\u6e2c\u8a66"), + metadata, + Column( + ue("\u6e2c\u8a66_id"), + Integer, + primary_key=True, + autoincrement=False, + ), + Column(ue("unitable1_\u6e2c\u8a66"), Integer), + Column(u("Unitéble2_b"), Integer), + Column(ue("\u6e2c\u8a66_self"), Integer), + test_needs_fk=True, + ) + + def test_insert(self, connection): + connection.execute(t1.insert(), {u("méil"): 1, ue("\u6e2c\u8a66"): 5}) + connection.execute(t2.insert(), {u("a"): 1, u("b"): 1}) + connection.execute( + t3.insert(), + { + ue("\u6e2c\u8a66_id"): 1, + ue("unitable1_\u6e2c\u8a66"): 5, + u("Unitéble2_b"): 1, + ue("\u6e2c\u8a66_self"): 1, + }, + ) + + eq_(connection.execute(t1.select()).fetchall(), [(1, 5)]) + eq_(connection.execute(t2.select()).fetchall(), [(1, 1)]) + eq_(connection.execute(t3.select()).fetchall(), [(1, 5, 1, 1)]) + + def test_col_targeting(self, connection): + connection.execute(t1.insert(), {u("méil"): 1, ue("\u6e2c\u8a66"): 5}) + connection.execute(t2.insert(), {u("a"): 1, u("b"): 1}) + connection.execute( + t3.insert(), + { + ue("\u6e2c\u8a66_id"): 1, + ue("unitable1_\u6e2c\u8a66"): 5, + u("Unitéble2_b"): 1, + ue("\u6e2c\u8a66_self"): 1, + }, + ) + + row = connection.execute(t1.select()).first() + eq_(row._mapping[t1.c[u("méil")]], 1) + eq_(row._mapping[t1.c[ue("\u6e2c\u8a66")]], 5) + + row = connection.execute(t2.select()).first() + eq_(row._mapping[t2.c[u("a")]], 1) + eq_(row._mapping[t2.c[u("b")]], 1) + + row = connection.execute(t3.select()).first() + eq_(row._mapping[t3.c[ue("\u6e2c\u8a66_id")]], 1) + eq_(row._mapping[t3.c[ue("unitable1_\u6e2c\u8a66")]], 5) + eq_(row._mapping[t3.c[u("Unitéble2_b")]], 1) + eq_(row._mapping[t3.c[ue("\u6e2c\u8a66_self")]], 1) + + def test_reflect(self, connection): + connection.execute(t1.insert(), {u("méil"): 2, ue("\u6e2c\u8a66"): 7}) + connection.execute(t2.insert(), {u("a"): 2, u("b"): 2}) + connection.execute( + t3.insert(), + { + ue("\u6e2c\u8a66_id"): 2, + ue("unitable1_\u6e2c\u8a66"): 7, + u("Unitéble2_b"): 2, + ue("\u6e2c\u8a66_self"): 2, + }, + ) + + meta = MetaData() + tt1 = Table(t1.name, meta, autoload_with=connection) + tt2 = Table(t2.name, meta, autoload_with=connection) + tt3 = Table(t3.name, meta, autoload_with=connection) + + connection.execute(tt1.insert(), {u("méil"): 1, ue("\u6e2c\u8a66"): 5}) + connection.execute(tt2.insert(), {u("méil"): 1, ue("\u6e2c\u8a66"): 1}) + connection.execute( + tt3.insert(), + { + ue("\u6e2c\u8a66_id"): 1, + ue("unitable1_\u6e2c\u8a66"): 5, + u("Unitéble2_b"): 1, + ue("\u6e2c\u8a66_self"): 1, + }, + ) + + eq_( + connection.execute( + tt1.select().order_by(desc(u("méil"))) + ).fetchall(), + [(2, 7), (1, 5)], + ) + eq_( + connection.execute( + tt2.select().order_by(desc(u("méil"))) + ).fetchall(), + [(2, 2), (1, 1)], + ) + eq_( + connection.execute( + tt3.select().order_by(desc(ue("\u6e2c\u8a66_id"))) + ).fetchall(), + [(2, 7, 2, 2), (1, 5, 1, 1)], + ) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..220480a --- /dev/null +++ b/tox.ini @@ -0,0 +1,30 @@ +[tox] +envlist = py27, py37, py38, py38_dc + +[testenv] +install_command=python -m pip install {env:TOX_PIP_OPTS:} {opts} {packages} +deps = + pytest>=4.6.11,<5.0; python_version < '3' + pytest>=6.2,<8; python_version >= '3' + mock; python_version < '3.3' + psycopg2>=2.8.6,<2.9; python_version < '3' + +base_command=python -m pytest --rootdir {toxinidir} --maxfail 1 + +passenv = LD_LIBRARY_PATH PYTHONPATH + +[testenv:py27] +basepython = python2.7 +commands = {[testenv]base_command} --db opengauss_psycopg2 {posargs} + +[testenv:py37] +basepython = python3.7 +commands = {[testenv]base_command} --db opengauss_psycopg2 {posargs} + +[testenv:py38] +basepython = python3.8 +commands = {[testenv]base_command} --db opengauss_psycopg2 {posargs} + +[testenv:py38_dc] +basepython = python3.8 +commands = {[testenv]base_command} --db opengauss_dc_psycopg2 {posargs} -- Gitee From f1efeba1975c82dd9b02ada3d81581f0b1f2654e Mon Sep 17 00:00:00 2001 From: jiajunsu Date: Tue, 8 Nov 2022 11:12:45 +0800 Subject: [PATCH 2/2] Modify copyright owner and add encoding header utf-8 --- LICENSE.md | 2 +- opengauss_sqlalchemy/__init__.py | 7 +++++-- opengauss_sqlalchemy/base.py | 6 +++--- opengauss_sqlalchemy/dc_psycopg2.py | 8 ++++++-- opengauss_sqlalchemy/provision.py | 7 +++---- opengauss_sqlalchemy/psycopg2.py | 8 ++++++-- opengauss_sqlalchemy/requirements.py | 6 +++--- test/conftest.py | 1 + test/test_compiler.py | 9 +++++++++ test/test_suite.py | 10 +++++++++- 10 files changed, 46 insertions(+), 18 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index eebf3b5..5801082 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,5 +1,5 @@ Copyright 2005-2022 SQLAlchemy authors and contributors . -Copyright (C) 2021-2022 Huawei. +Copyright (C) 2021-2022 Huawei Technologies Co.,Ltd. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/opengauss_sqlalchemy/__init__.py b/opengauss_sqlalchemy/__init__.py index 1a16f00..20b823e 100644 --- a/opengauss_sqlalchemy/__init__.py +++ b/opengauss_sqlalchemy/__init__.py @@ -1,5 +1,8 @@ -# opengauss_sqlalchemy/__init__.py -# Copyright (C) 2021-2022 Huawei. +# Copyright (C) 2005-2022 the SQLAlchemy authors and contributors +# +# +# Copyright (C) 2021-2022 Huawei Technologies Co.,Ltd. +# # This module is part of SQLAlchemy and is released under # the MIT License: https://www.opensource.org/licenses/mit-license.php diff --git a/opengauss_sqlalchemy/base.py b/opengauss_sqlalchemy/base.py index 933516e..ae6c9d8 100644 --- a/opengauss_sqlalchemy/base.py +++ b/opengauss_sqlalchemy/base.py @@ -1,8 +1,8 @@ -# opengauss_sqlalchemy/base.py +# -*- coding: utf-8 -*- # Copyright (C) 2005-2022 the SQLAlchemy authors and contributors # -# This source file has been modified by Huawei. -# Copyright (C) 2021-2022 Huawei. +# +# Copyright (C) 2021-2022 Huawei Technologies Co.,Ltd. # # This module is part of SQLAlchemy and is released under # the MIT License: https://www.opensource.org/licenses/mit-license.php diff --git a/opengauss_sqlalchemy/dc_psycopg2.py b/opengauss_sqlalchemy/dc_psycopg2.py index 328b9f0..9163b44 100644 --- a/opengauss_sqlalchemy/dc_psycopg2.py +++ b/opengauss_sqlalchemy/dc_psycopg2.py @@ -1,5 +1,9 @@ -# opengauss_sqlalchemy/dc_psycopg2.py -# Copyright (C) 2021-2022 Huawei. +# -*- coding: utf-8 -*- +# Copyright (C) 2005-2022 the SQLAlchemy authors and contributors +# +# +# Copyright (C) 2021-2022 Huawei Technologies Co.,Ltd. +# # This module is part of SQLAlchemy and is released under # the MIT License: https://www.opensource.org/licenses/mit-license.php diff --git a/opengauss_sqlalchemy/provision.py b/opengauss_sqlalchemy/provision.py index 6f21d0f..802a9d3 100644 --- a/opengauss_sqlalchemy/provision.py +++ b/opengauss_sqlalchemy/provision.py @@ -1,8 +1,8 @@ -# opengauss_sqlalchemy/provision.py +# -*- coding: utf-8 -*- # Copyright (C) 2005-2022 the SQLAlchemy authors and contributors # -# This source file has been modified by Huawei. -# Copyright (C) 2021-2022 Huawei. +# +# Copyright (C) 2021-2022 Huawei Technologies Co.,Ltd. # # This module is part of SQLAlchemy and is released under # the MIT License: https://www.opensource.org/licenses/mit-license.php @@ -121,7 +121,6 @@ def prepare_for_drop_tables(config, connection): result = connection.exec_driver_sql( "select pid, state, wait_event_type, query " - # "select pg_terminate_backend(pid), state, wait_event_type " "from pg_stat_activity where " "usename=current_user " "and datname=current_database() and state='idle in transaction' " diff --git a/opengauss_sqlalchemy/psycopg2.py b/opengauss_sqlalchemy/psycopg2.py index 50c275f..007565a 100644 --- a/opengauss_sqlalchemy/psycopg2.py +++ b/opengauss_sqlalchemy/psycopg2.py @@ -1,5 +1,9 @@ -# opengauss_sqlalchemy/psycopg2.py -# Copyright (C) 2021-2022 Huawei. +# -*- coding: utf-8 -*- +# Copyright (C) 2005-2022 the SQLAlchemy authors and contributors +# +# +# Copyright (C) 2021-2022 Huawei Technologies Co.,Ltd. +# # This module is part of SQLAlchemy and is released under # the MIT License: https://www.opensource.org/licenses/mit-license.php diff --git a/opengauss_sqlalchemy/requirements.py b/opengauss_sqlalchemy/requirements.py index 0a38baf..3085b38 100644 --- a/opengauss_sqlalchemy/requirements.py +++ b/opengauss_sqlalchemy/requirements.py @@ -1,8 +1,8 @@ -# sqlalchemy_opengauss/requirements.py +# -*- coding: utf-8 -*- # Copyright (C) 2005-2022 the SQLAlchemy authors and contributors # -# This source file has been modified by Huawei. -# Copyright (C) 2021-2022 Huawei. +# +# Copyright (C) 2021-2022 Huawei Technologies Co.,Ltd. # # This module is part of SQLAlchemy and is released under # the MIT License: https://www.opensource.org/licenses/mit-license.php diff --git a/test/conftest.py b/test/conftest.py index 9d7492b..9f07a44 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from sqlalchemy.testing.plugin.pytestplugin import * # noqa import opengauss_sqlalchemy # noqa diff --git a/test/test_compiler.py b/test/test_compiler.py index 35680ca..0ef2c8b 100644 --- a/test/test_compiler.py +++ b/test/test_compiler.py @@ -1,3 +1,12 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2005-2022 the SQLAlchemy authors and contributors +# +# +# Copyright (C) 2021-2022 Huawei Technologies Co.,Ltd. +# +# This module is part of SQLAlchemy and is released under +# the MIT License: https://www.opensource.org/licenses/mit-license.php + from sqlalchemy import ( and_, Column, exc, Float, func, Index, Integer, MetaData, Numeric, schema, String, Table, text ) diff --git a/test/test_suite.py b/test/test_suite.py index 812b44a..a226f42 100644 --- a/test/test_suite.py +++ b/test/test_suite.py @@ -1,4 +1,12 @@ -# coding: utf-8 +# -*- coding: utf-8 -*- +# Copyright (C) 2005-2022 the SQLAlchemy authors and contributors +# +# +# Copyright (C) 2021-2022 Huawei Technologies Co.,Ltd. +# +# This module is part of SQLAlchemy and is released under +# the MIT License: https://www.opensource.org/licenses/mit-license.php + import operator import sqlalchemy as sa -- Gitee