Преглед изворни кода

HUE-9367 [lib] Update phoenixdb with PHOENIX-5994

SqlAlchemy schema filtering incorrect semantics
https://issues.apache.org/jira/browse/PHOENIX-5994
Romain пре 5 година
родитељ
комит
76bd01532c
32 измењених фајлова са 1278 додато и 153 уклоњено
  1. 13 0
      desktop/core/ext-py/phoenixdb/.gitignore
  2. 149 0
      desktop/core/ext-py/phoenixdb/.gitlab-ci.yml
  3. 16 1
      desktop/core/ext-py/phoenixdb/Dockerfile
  4. 16 1
      desktop/core/ext-py/phoenixdb/Dockerfile-pqs
  5. 202 0
      desktop/core/ext-py/phoenixdb/LICENSE
  6. 3 0
      desktop/core/ext-py/phoenixdb/NEWS.rst
  7. 10 0
      desktop/core/ext-py/phoenixdb/NOTICE
  8. 22 9
      desktop/core/ext-py/phoenixdb/README.rst
  9. 15 0
      desktop/core/ext-py/phoenixdb/ci/build-env/Dockerfile
  10. 15 0
      desktop/core/ext-py/phoenixdb/ci/phoenix/Dockerfile
  11. 15 0
      desktop/core/ext-py/phoenixdb/ci/phoenix/docker-entrypoint.sh
  12. 17 0
      desktop/core/ext-py/phoenixdb/ci/phoenix/hbase-site.xml
  13. 139 0
      desktop/core/ext-py/phoenixdb/dev-support/cache-apache-project-artifact.sh
  14. 4 0
      desktop/core/ext-py/phoenixdb/dev-support/rat-excludes.txt
  15. 44 0
      desktop/core/ext-py/phoenixdb/dev-support/run-source-ratcheck.sh
  16. 15 0
      desktop/core/ext-py/phoenixdb/doc/Makefile
  17. 16 0
      desktop/core/ext-py/phoenixdb/doc/api.rst
  18. 15 0
      desktop/core/ext-py/phoenixdb/doc/conf.py
  19. 15 1
      desktop/core/ext-py/phoenixdb/doc/index.rst
  20. 16 0
      desktop/core/ext-py/phoenixdb/doc/versions.rst
  21. 20 0
      desktop/core/ext-py/phoenixdb/gen-protobuf.sh
  22. 69 38
      desktop/core/ext-py/phoenixdb/phoenixdb/avatica/client.py
  23. 14 0
      desktop/core/ext-py/phoenixdb/phoenixdb/avatica/proto/__init__.py
  24. 14 0
      desktop/core/ext-py/phoenixdb/phoenixdb/avatica/proto/common_pb2.py
  25. 14 0
      desktop/core/ext-py/phoenixdb/phoenixdb/avatica/proto/requests_pb2.py
  26. 14 0
      desktop/core/ext-py/phoenixdb/phoenixdb/avatica/proto/responses_pb2.py
  27. 62 38
      desktop/core/ext-py/phoenixdb/phoenixdb/connection.py
  28. 8 6
      desktop/core/ext-py/phoenixdb/phoenixdb/cursor.py
  29. 96 0
      desktop/core/ext-py/phoenixdb/phoenixdb/meta.py
  30. 51 57
      desktop/core/ext-py/phoenixdb/phoenixdb/sqlalchemy_phoenix.py
  31. 119 0
      desktop/core/ext-py/phoenixdb/phoenixdb/tests/test_db.py
  32. 40 2
      desktop/core/ext-py/phoenixdb/phoenixdb/tests/test_sqlalchemy.py

+ 13 - 0
desktop/core/ext-py/phoenixdb/.gitignore

@@ -0,0 +1,13 @@
+/dist/
+/build/
+/doc/_build/
+/doc/build/
+*.pyc
+*.egg-info/
+.vagrant/
+.tox
+dev-support/artifacts
+dev-support/work
+phoenixdb/.eggs
+phoenixdb/build
+phoenixdb/e

+ 149 - 0
desktop/core/ext-py/phoenixdb/.gitlab-ci.yml

@@ -0,0 +1,149 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+stages:
+  - prepare
+  - test
+
+build build-env image:
+  stage: prepare
+  script:
+    - cd ci/build-env
+    - docker build -t ${CI_REGISTRY_IMAGE}/build-env .
+    - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
+    - docker push $CI_REGISTRY_IMAGE/build-env
+  tags:
+    - docker-host
+  only:
+    - master@lukas/python-phoenixdb
+
+.build-phoenix-image: &build_phoenix_image
+  stage: prepare
+  script:
+    - JOB_NAME=($CI_JOB_NAME)
+    - cd ci/phoenix
+    - docker build -t ${CI_REGISTRY_IMAGE}/phoenix:${JOB_NAME[2]}
+        --build-arg PHOENIX_VERSION=$PHOENIX_VERSION
+        --build-arg HBASE_VERSION=$HBASE_VERSION
+        --build-arg HBASE_DIR=$HBASE_DIR
+        .
+    - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
+    - docker push $CI_REGISTRY_IMAGE/phoenix:${JOB_NAME[2]}
+  tags:
+    - docker-host
+
+build phoenix 5.0.0-alpha-HBase-2.0 image:
+  <<: *build_phoenix_image
+  variables:
+    PHOENIX_VERSION: 5.0.0-alpha-HBase-2.0
+    HBASE_VERSION: 2.0.0-beta-1
+    HBASE_DIR: hbase-2.0.0-beta-1
+
+build phoenix 4.13 image:
+  <<: *build_phoenix_image
+  variables:
+    PHOENIX_VERSION: 4.13.1-HBase-1.3
+    HBASE_VERSION: 1.3.1
+    HBASE_DIR: 1.3.1
+
+build phoenix 4.12 image:
+  <<: *build_phoenix_image
+  variables:
+    PHOENIX_VERSION: 4.12.0-HBase-1.3
+    HBASE_VERSION: 1.3.1
+    HBASE_DIR: 1.3.1
+
+build phoenix 4.11 image:
+  <<: *build_phoenix_image
+  variables:
+    PHOENIX_VERSION: 4.11.0-HBase-1.3
+    HBASE_VERSION: 1.3.1
+    HBASE_DIR: 1.3.1
+
+build phoenix 4.10 image:
+  <<: *build_phoenix_image
+  variables:
+    PHOENIX_VERSION: 4.10.0-HBase-1.2
+    HBASE_VERSION: 1.2.6
+    HBASE_DIR: 1.2.6
+
+build phoenix 4.9 image:
+  <<: *build_phoenix_image
+  variables:
+    PHOENIX_VERSION: 4.9.0-HBase-1.2
+    HBASE_VERSION: 1.2.6
+    HBASE_DIR: 1.2.6
+
+build phoenix 4.8 image:
+  <<: *build_phoenix_image
+  variables:
+    PHOENIX_VERSION: 4.8.2-HBase-1.2
+    HBASE_VERSION: 1.2.6
+    HBASE_DIR: 1.2.6
+
+.test: &test
+  image: $CI_REGISTRY_IMAGE/build-env
+  variables:
+    PHOENIXDB_TEST_DB_URL: http://phoenix:8765/
+    PIP_CACHE_DIR: $CI_PROJECT_DIR/cache/
+  script:
+    - tox -e py27,py35
+  cache:
+    paths:
+      - cache/
+  tags:
+    - docker
+
+test phoenix 5.0.0-alpha-HBase-2.0:
+  <<: *test
+  services:
+    - name: $CI_REGISTRY_IMAGE/phoenix:5.0.0-alpha-HBase-2.0
+      alias: phoenix
+
+test phoenix 4.13:
+  <<: *test
+  services:
+    - name: $CI_REGISTRY_IMAGE/phoenix:4.13
+      alias: phoenix
+
+test phoenix 4.12:
+  <<: *test
+  services:
+    - name: $CI_REGISTRY_IMAGE/phoenix:4.12
+      alias: phoenix
+
+test phoenix 4.11:
+  <<: *test
+  services:
+    - name: $CI_REGISTRY_IMAGE/phoenix:4.11
+      alias: phoenix
+
+test phoenix 4.10:
+  <<: *test
+  services:
+    - name: $CI_REGISTRY_IMAGE/phoenix:4.10
+      alias: phoenix
+
+test phoenix 4.9:
+  <<: *test
+  services:
+    - name: $CI_REGISTRY_IMAGE/phoenix:4.9
+      alias: phoenix
+
+test phoenix 4.8:
+  <<: *test
+  services:
+    - name: $CI_REGISTRY_IMAGE/phoenix:4.8
+      alias: phoenix

+ 16 - 1
desktop/core/ext-py/phoenixdb/Dockerfile

@@ -1,3 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
 from themattrix/tox-base
 
 RUN apt-get update && apt-get install -y krb5-user libkrb5-dev
@@ -6,4 +21,4 @@ ENV PHOENIXDB_TEST_DB_URL=http://host.docker.internal:8765
 ENV PHOENIXDB_TEST_DB_TRUSTSTORE=
 ENV PHOENIXDB_TEST_DB_AUTHENTICATION=
 ENV PHOENIXDB_TEST_DB_AVATICA_USER=
-ENV PHOENIXDB_TEST_DB_AVATICA_PASSWORD=
+ENV PHOENIXDB_TEST_DB_AVATICA_PASSWORD=

+ 16 - 1
desktop/core/ext-py/phoenixdb/Dockerfile-pqs

@@ -1,3 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
 from maven:3-jdk-8
 
 RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -yq krb5-user libkrb5-dev
@@ -6,4 +21,4 @@ EXPOSE 8765
 
 # copy all the files to the container
 
-CMD mvn clean verify -am -pl queryserver-it -Dtest=foo -Dit.test=QueryServerBasicsIT#startLocalPQS -Ddo.not.randomize.pqs.port=true -Dstart.unsecure.pqs=true
+CMD mvn clean verify -am -pl queryserver-it -Dtest=foo -Dit.test=QueryServerBasicsIT#startLocalPQS -Ddo.not.randomize.pqs.port=true -Dstart.unsecure.pqs=true

+ 202 - 0
desktop/core/ext-py/phoenixdb/LICENSE

@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 3 - 0
desktop/core/ext-py/phoenixdb/NEWS.rst

@@ -16,6 +16,9 @@ Unreleased
 - Removed shell example, as it was python2 only
 - Updated documentation
 - Added SQLAlchemy dialect
+- Implemented Avatica Metadata API
+- Misc fixes
+- Licensing cleanup
 
 Version 0.7
 -----------

+ 10 - 0
desktop/core/ext-py/phoenixdb/NOTICE

@@ -0,0 +1,10 @@
+Apache Phoenix -- PhoenixDB
+Copyright 2020 The Apache Software Foundation
+
+This product includes software developed by The Apache Software
+Foundation (http://www.apache.org/).
+
+This project was originally created by Lukas Lalinsky, copyright 2015.
+
+This project contains phoenixdb/phoenixdb/sqlalchemy_phoenix.py which is a modification from
+https://github.com/Pirionfr/pyPhoenix, authored by Dimitri Capitaine, copyright 2017.

+ 22 - 9
desktop/core/ext-py/phoenixdb/README.rst

@@ -1,13 +1,14 @@
 Phoenix database adapter for Python
 ===================================
 
-``phoenixdb`` is a Python library for accessing the
-`Phoenix SQL database <http://phoenix.apache.org/>`_
+``phoenixdb`` is a Python library for accessing 
+`Apache Phoenix <http://phoenix.apache.org/>`_
 using the
 `remote query server <http://phoenix.apache.org/server.html>`_.
-The library implements the
-standard `DB API 2.0 <https://www.python.org/dev/peps/pep-0249/>`_ interface,
-which should be familiar to most Python programmers.
+This library implements the
+standard `DB API 2.0 <https://www.python.org/dev/peps/pep-0249/>`_ interface and a
+subset of `SQLAlchemy <https://www.sqlalchemy.org/>`_, either of which should be familiar
+to most Python programmers.
 
 Installation
 ------------
@@ -59,18 +60,18 @@ necessary requirements::
 You can start a Phoenix QueryServer instance on http://localhost:8765 for testing by running
 the following command in the phoenix-queryserver directory::
 
-    mvn clean verify -am -pl queryserver-it -Dtest=foo \
+    mvn clean verify -am -pl phoenix-queryserver-it -Dtest=foo \
     -Dit.test=QueryServerBasicsIT\#startLocalPQS \
     -Ddo.not.randomize.pqs.port=true -Dstart.unsecure.pqs=true
 
 You can start a secure (https+kerberos) Phoenix QueryServer instance on https://localhost:8765
 for testing by running the following command in the phoenix-queryserver directory::
 
-    mvn clean verify -am -pl queryserver-it -Dtest=foo \
+    mvn clean verify -am -pl phoenix-queryserver-it -Dtest=foo \
     -Dit.test=SecureQueryServerPhoenixDBIT\#startLocalPQS \
     -Ddo.not.randomize.pqs.port=true -Dstart.secure.pqs=true
 
-this will also create a shell script in queryserver-it/target/krb_setup.sh, that you can use to set
+this will also create a shell script in phoenix-queryserver-it/target/krb_setup.sh, that you can use to set
 up the environment for the tests.
 
 If you want to use the library without installing the phoenixdb library, you can use
@@ -118,7 +119,7 @@ environments locally::
 You can also run the test suite from maven as part of the Java build by setting the 
 run.full.python.testsuite property. You DO NOT need to set the PHOENIXDB_* enviroment variables,
 maven will set them up for you. The output of the test run will be saved in
-phoenix-queryserver/queryserver-it/target/python-stdout.log and python-stderr.log::
+phoenix-queryserver/phoenix-queryserver-it/target/python-stdout.log and python-stderr.log::
 
     mvn clean verify -Drun.full.python.testsuite=true
 
@@ -129,3 +130,15 @@ Known issues
   but the remote protocol only exposes the time (hour/minute/second) or date (year/month/day)
   parts of the columns. (`CALCITE-797 <https://issues.apache.org/jira/browse/CALCITE-797>`_, `CALCITE-798 <https://issues.apache.org/jira/browse/CALCITE-798>`_)
 - TIMESTAMP columns in Phoenix are stored with a nanosecond accuracy, but the remote protocol truncates them to milliseconds. (`CALCITE-796 <https://issues.apache.org/jira/browse/CALCITE-796>`_)
+
+
+SQLAlchemy feature support
+--------------------------
+
+SQLAlchemy has a wide breadth of API, ranging from basic SQL commands to object-relational mapping support.
+
+Today, python-phoenixdb only supports the following subset of the complete SQLAlchemy API:
+
+- `Textual SQL <https://docs.sqlalchemy.org/en/13/core/tutorial.html#using-textual-sql>`_
+
+All other API should be considered not implemented.

+ 15 - 0
desktop/core/ext-py/phoenixdb/ci/build-env/Dockerfile

@@ -1,3 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
 FROM ubuntu:xenial
 
 RUN apt-get update && \

+ 15 - 0
desktop/core/ext-py/phoenixdb/ci/phoenix/Dockerfile

@@ -1,3 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
 FROM openjdk:8
 
 ARG HBASE_VERSION

+ 15 - 0
desktop/core/ext-py/phoenixdb/ci/phoenix/docker-entrypoint.sh

@@ -1,4 +1,19 @@
 #!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
 
 pids=()
 

+ 17 - 0
desktop/core/ext-py/phoenixdb/ci/phoenix/hbase-site.xml

@@ -1,5 +1,22 @@
 <?xml version="1.0"?>
 <?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+ 
+      http://www.apache.org/licenses/LICENSE-2.0
+ 
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
 <configuration>
     <property>
         <name>hbase.regionserver.wal.codec</name>

+ 139 - 0
desktop/core/ext-py/phoenixdb/dev-support/cache-apache-project-artifact.sh

@@ -0,0 +1,139 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# This was lovingly copied from Apache HBase
+
+set -e
+function usage {
+  echo "Usage: ${0} [options] /path/to/download/file.tar.gz download/fragment/eg/project/subdir/some-artifact-version.tar.gz"
+  echo ""
+  echo "    --force                       for a redownload even if /path/to/download/file.tar.gz exists."
+  echo "    --working-dir /path/to/use    Path for writing tempfiles. must exist."
+  echo "                                  defaults to making a directory via mktemp that we clean."
+  echo "    --keys url://to/project/KEYS  where to get KEYS. needed to check signature on download."
+  echo ""
+  exit 1
+}
+# if no args specified, show usage
+if [ $# -lt 2 ]; then
+  usage
+fi
+
+
+# Get arguments
+declare done_if_cached="true"
+declare working_dir
+declare cleanup="true"
+declare keys
+while [ $# -gt 0 ]
+do
+  case "$1" in
+    --force) shift; done_if_cached="false";;
+    --working-dir) shift; working_dir=$1; cleanup="false"; shift;;
+    --keys) shift; keys=$1; shift;;
+    --) shift; break;;
+    -*) usage ;;
+    *)  break;;  # terminate while loop
+  esac
+done
+
+# should still have required args
+if [ $# -lt 2 ]; then
+  usage
+fi
+
+target="$1"
+artifact="$2"
+
+if [ -f "${target}" ] && [ "true" = "${done_if_cached}" ]; then
+  echo "Reusing existing download of '${artifact}'."
+  exit 0
+fi
+
+if [ -z "${working_dir}" ]; then
+  if ! working_dir="$(mktemp -d -t hbase-download-apache-artifact)" ; then
+    echo "Failed to create temporary working directory. Please specify via --working-dir" >&2
+    exit 1
+  fi
+else
+  # absolutes please
+  working_dir="$(cd "$(dirname "${working_dir}")"; pwd)/$(basename "${working_dir}")"
+  if [ ! -d "${working_dir}" ]; then
+    echo "passed working directory '${working_dir}' must already exist." >&2
+    exit 1
+  fi
+fi
+
+function cleanup {
+  if [ -n "${keys}" ]; then
+    echo "Stopping gpg agent daemon"
+    gpgconf --homedir "${working_dir}/.gpg" --kill gpg-agent
+    echo "Stopped gpg agent daemon"
+  fi
+
+  if [ "true" = "${cleanup}" ]; then
+    echo "cleaning up temp space."
+    rm -rf "${working_dir}"
+  fi
+}
+trap cleanup EXIT SIGQUIT
+
+echo "New download of '${artifact}'"
+
+# N.B. this comes first so that if gpg falls over we skip the expensive download.
+if [ -n "${keys}" ]; then
+  if [ ! -d "${working_dir}/.gpg" ]; then
+    rm -rf "${working_dir}/.gpg"
+    mkdir -p "${working_dir}/.gpg"
+    chmod -R 700 "${working_dir}/.gpg"
+  fi
+
+  echo "installing project KEYS"
+  curl -L --fail -o "${working_dir}/KEYS" "${keys}"
+  if ! gpg --homedir "${working_dir}/.gpg" --import "${working_dir}/KEYS" ; then
+    echo "ERROR importing the keys via gpg failed. If the output above mentions this error:" >&2
+    echo "    gpg: can't connect to the agent: File name too long" >&2
+    # we mean to give them the command to run, not to run it.
+    #shellcheck disable=SC2016
+    echo 'then you prolly need to create /var/run/user/$(id -u)' >&2
+    echo "see this thread on gnupg-users: https://s.apache.org/uI7x" >&2
+    exit 2
+  fi
+
+  echo "downloading signature"
+  curl -L --fail -o "${working_dir}/artifact.asc" "https://archive.apache.org/dist/${artifact}.asc"
+fi
+
+echo "downloading artifact"
+if ! curl --dump-header "${working_dir}/artifact_download_headers.txt" -L --fail -o "${working_dir}/artifact" "https://www.apache.org/dyn/closer.lua?filename=${artifact}&action=download" ; then
+  echo "Artifact wasn't in mirror system. falling back to archive.a.o."
+  curl --dump-header "${working_dir}/artifact_fallback_headers.txt" -L --fail -o "${working_dir}/artifact" "http://archive.apache.org/dist/${artifact}"
+fi
+
+if [ -n "${keys}" ]; then
+  echo "verifying artifact signature"
+  gpg --homedir "${working_dir}/.gpg" --verify "${working_dir}/artifact.asc"
+  echo "signature good."
+fi
+
+echo "moving artifact into place at '${target}'"
+# ensure we're on the same filesystem
+mv "${working_dir}/artifact" "${target}.copying"
+# attempt atomic move
+mv "${target}.copying" "${target}"
+echo "all done!"

+ 4 - 0
desktop/core/ext-py/phoenixdb/dev-support/rat-excludes.txt

@@ -0,0 +1,4 @@
+.*\.pyc
+NEWS\.rst
+RELEASING\.rst
+README\.rst

+ 44 - 0
desktop/core/ext-py/phoenixdb/dev-support/run-source-ratcheck.sh

@@ -0,0 +1,44 @@
+#!/usr/bin/env bash
+
+# Catch some more errors
+set -eu
+set -o pipefail
+
+# The name of the Apache RAT CLI binary file
+RAT_BINARY_NAME="apache-rat-0.13-bin.tar.gz"
+# The relative path on the ASF mirrors for the RAT binary file
+RAT_BINARY_MIRROR_NAME="creadur/apache-rat-0.13/$RAT_BINARY_NAME"
+RAT_BINARY_DIR="apache-rat-0.13"
+RAT_JAR="$RAT_BINARY_DIR.jar"
+
+# Constants
+DEV_SUPPORT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
+ARTIFACTS_DIR="$DEV_SUPPORT/artifacts"
+WORK_DIR="$DEV_SUPPORT/work"
+
+mkdir -p "$WORK_DIR" "$ARTIFACTS_DIR"
+
+# Cache the RAT binary artifacts
+if [[ ! -f "$ARTIFACTS_DIR/$RAT_BINARY_NAME" ]]; then
+  echo "$ARTIFACTS_DIR/$RAT_BINARY_NAME does not exist, downloading it"
+  $DEV_SUPPORT/cache-apache-project-artifact.sh --working-dir "$WORK_DIR" --keys https://www.apache.org/dist/creadur/KEYS \
+    "$ARTIFACTS_DIR/$RAT_BINARY_NAME" "$RAT_BINARY_MIRROR_NAME"
+fi
+
+# Extract the RAT binary artifacts
+if [[ ! -d "$ARTIFACTS_DIR/$RAT_BINARY_DIR" ]]; then
+  echo "$ARTIFACTS_DIR/$RAT_BINARY_DIR does not exist, extracting $ARTIFACTS_DIR/$RAT_BINARY_NAME"
+  tar xf $ARTIFACTS_DIR/$RAT_BINARY_NAME -C $ARTIFACTS_DIR
+fi
+
+echo "RAT binary installation localized, running RAT check"
+
+# Run the RAT check, excluding pyc files
+for src in 'phoenixdb' 'ci' 'examples' 'doc'; do 
+  echo "Running RAT check over $src"
+  java -jar "$ARTIFACTS_DIR/$RAT_BINARY_DIR/$RAT_JAR" -d "$DEV_SUPPORT/../$src" -E "$DEV_SUPPORT/rat-excludes.txt"
+  if [[ $? -ne 0 ]]; then
+    echo "Failed RAT check over $src"
+    exit 1
+  fi
+done

+ 15 - 0
desktop/core/ext-py/phoenixdb/doc/Makefile

@@ -1,3 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
 # Makefile for Sphinx documentation
 #
 

+ 16 - 0
desktop/core/ext-py/phoenixdb/doc/api.rst

@@ -1,3 +1,19 @@
+..
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
 API Reference
 =============
 

+ 15 - 0
desktop/core/ext-py/phoenixdb/doc/conf.py

@@ -1,5 +1,20 @@
 # -*- coding: utf-8 -*-
+
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
 #
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
 # phoenixdb documentation build configuration file, created by
 # sphinx-quickstart on Sun Jun 28 18:07:35 2015.
 #

+ 15 - 1
desktop/core/ext-py/phoenixdb/doc/index.rst

@@ -1,4 +1,18 @@
-.. include:: ../README.rst
+..
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License... include:: ../README.rst
 
 API Reference
 -------------

+ 16 - 0
desktop/core/ext-py/phoenixdb/doc/versions.rst

@@ -1,3 +1,19 @@
+..
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
 .. include:: ../NEWS.rst
 
 .. _

+ 20 - 0
desktop/core/ext-py/phoenixdb/gen-protobuf.sh

@@ -36,4 +36,24 @@ else
   sed -i 's/import common_pb2/from . import common_pb2/' phoenixdb/avatica/proto/*_pb2.py
 fi
 
+for f in $(find phoenixdb/avatica/proto -name '*.py'); do
+  cat << EOF > ${f}-with-header
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+EOF
+  cat $f >> ${f}-with-header
+done
+
 rm -rf avatica-tmp

+ 69 - 38
desktop/core/ext-py/phoenixdb/phoenixdb/avatica/client.py

@@ -1,10 +1,13 @@
 # Copyright 2015 Lukas Lalinsky
 #
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
 #
-#    http://www.apache.org/licenses/LICENSE-2.0
+#     http://www.apache.org/licenses/LICENSE-2.0
 #
 # Unless required by applicable law or agreed to in writing, software
 # distributed under the License is distributed on an "AS IS" BASIS,
@@ -12,7 +15,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""Implementation of the JSON-over-HTTP RPC protocol used by Avatica."""
+"""Implementation of the PROTOBUF-over-HTTP RPC protocol used by Avatica."""
 
 import logging
 import math
@@ -85,24 +88,12 @@ SQLSTATE_ERROR_CLASSES = [
     ('INT', errors.InternalError),  # Phoenix internal error
 ]
 
-# Relevant properties as defined by https://calcite.apache.org/avatica/docs/client_reference.html
-OPEN_CONNECTION_PROPERTIES = (
-    'avatica_user',  # User for the database connection
-    'avatica_password',  # Password for the user
-    'auth',
-    'authentication',
-    'truststore',
-    'verify',
-    'do_as',
-    'user',
-    'password'
-)
-
 
 def raise_sql_error(code, sqlstate, message):
     for prefix, error_class in SQLSTATE_ERROR_CLASSES:
         if sqlstate.startswith(prefix):
             raise error_class(message, code, sqlstate)
+    raise errors.InternalError(message, code, sqlstate)
 
 
 def parse_and_raise_sql_error(message):
@@ -122,15 +113,20 @@ def parse_error_page(html):
 
 
 def parse_error_protobuf(text):
-    message = common_pb2.WireMessage()
-    message.ParseFromString(text)
+    try:
+        message = common_pb2.WireMessage()
+        message.ParseFromString(text)
 
-    err = responses_pb2.ErrorResponse()
-    err.ParseFromString(message.wrapped_message)
+        err = responses_pb2.ErrorResponse()
+        if not err.ParseFromString(message.wrapped_message):
+            raise Exception('No error message found')
+    except Exception:
+        # Not a protobuf error, fall through
+        return
 
     parse_and_raise_sql_error(err.error_message)
     raise_sql_error(err.error_code, err.sql_state, err.error_message)
-    raise errors.InternalError(err.error_message)
+    # Not a protobuf error, fall through
 
 
 class AvaticaClient(object):
@@ -236,7 +232,10 @@ class AvaticaClient(object):
     def get_catalogs(self, connection_id):
         request = requests_pb2.CatalogsRequest()
         request.connection_id = connection_id
-        return self._apply(request)
+        response_data = self._apply(request, 'ResultSetResponse')
+        response = responses_pb2.ResultSetResponse()
+        response.ParseFromString(response_data)
+        return response
 
     def get_schemas(self, connection_id, catalog=None, schemaPattern=None):
         request = requests_pb2.SchemasRequest()
@@ -245,7 +244,10 @@ class AvaticaClient(object):
             request.catalog = catalog
         if schemaPattern is not None:
             request.schema_pattern = schemaPattern
-        return self._apply(request)
+        response_data = self._apply(request, 'ResultSetResponse')
+        response = responses_pb2.ResultSetResponse()
+        response.ParseFromString(response_data)
+        return response
 
     def get_tables(self, connection_id, catalog=None, schemaPattern=None, tableNamePattern=None, typeList=None):
         request = requests_pb2.TablesRequest()
@@ -256,12 +258,13 @@ class AvaticaClient(object):
             request.schema_pattern = schemaPattern
         if tableNamePattern is not None:
             request.table_name_pattern = tableNamePattern
-        if typeList is not None:
-            request.type_list = typeList
         if typeList is not None:
             request.type_list.extend(typeList)
         request.has_type_list = typeList is not None
-        return self._apply(request)
+        response_data = self._apply(request, 'ResultSetResponse')
+        response = responses_pb2.ResultSetResponse()
+        response.ParseFromString(response_data)
+        return response
 
     def get_columns(self, connection_id, catalog=None, schemaPattern=None, tableNamePattern=None, columnNamePattern=None):
         request = requests_pb2.ColumnsRequest()
@@ -274,17 +277,35 @@ class AvaticaClient(object):
             request.table_name_pattern = tableNamePattern
         if columnNamePattern is not None:
             request.column_name_pattern = columnNamePattern
-        return self._apply(request)
+        response_data = self._apply(request, 'ResultSetResponse')
+        response = responses_pb2.ResultSetResponse()
+        response.ParseFromString(response_data)
+        return response
 
     def get_table_types(self, connection_id):
         request = requests_pb2.TableTypesRequest()
         request.connection_id = connection_id
-        return self._apply(request)
+        response_data = self._apply(request, 'ResultSetResponse')
+        response = responses_pb2.ResultSetResponse()
+        response.ParseFromString(response_data)
+        return response
 
     def get_type_info(self, connection_id):
         request = requests_pb2.TypeInfoRequest()
         request.connection_id = connection_id
-        return self._apply(request)
+        response_data = self._apply(request, 'ResultSetResponse')
+        response = responses_pb2.ResultSetResponse()
+        response.ParseFromString(response_data)
+        return response
+
+    def connection_sync_dict(self, connection_id, connProps=None):
+        conn_props = self.connection_sync(connection_id, connProps)
+        return {
+            'autoCommit': conn_props.auto_commit,
+            'readOnly': conn_props.read_only,
+            'transactionIsolation': conn_props.transaction_isolation,
+            'catalog': conn_props.catalog,
+            'schema': conn_props.schema}
 
     def connection_sync(self, connection_id, connProps=None):
         """Synchronizes connection properties with the server.
@@ -298,18 +319,28 @@ class AvaticaClient(object):
         :returns:
             A ``common_pb2.ConnectionProperties`` object.
         """
-        if connProps is None:
-            connProps = {}
+        if connProps:
+            props = connProps.copy()
+        else:
+            props = {}
 
         request = requests_pb2.ConnectionSyncRequest()
         request.connection_id = connection_id
-        request.conn_props.auto_commit = connProps.get('autoCommit', False)
         request.conn_props.has_auto_commit = True
-        request.conn_props.read_only = connProps.get('readOnly', False)
         request.conn_props.has_read_only = True
-        request.conn_props.transaction_isolation = connProps.get('transactionIsolation', 0)
-        request.conn_props.catalog = connProps.get('catalog', '')
-        request.conn_props.schema = connProps.get('schema', '')
+        if 'autoCommit' in props:
+            request.conn_props.auto_commit = props.pop('autoCommit')
+        if 'readOnly' in props:
+            request.conn_props.read_only = props.pop('readOnly')
+        if 'transactionIsolation' in props:
+            request.conn_props.transaction_isolation = props.pop('transactionIsolation', None)
+        if 'catalog' in props:
+            request.conn_props.catalog = props.pop('catalog', None)
+        if 'schema' in props:
+            request.conn_props.schema = props.pop('schema', None)
+
+        if props:
+            logger.warning("Unhandled connection property:" + props)
 
         response_data = self._apply(request)
         response = responses_pb2.ConnectionSyncResponse()

+ 14 - 0
desktop/core/ext-py/phoenixdb/phoenixdb/avatica/proto/__init__.py

@@ -0,0 +1,14 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.

+ 14 - 0
desktop/core/ext-py/phoenixdb/phoenixdb/avatica/proto/common_pb2.py

@@ -1,3 +1,17 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
 # Generated by the protocol buffer compiler.  DO NOT EDIT!
 # source: common.proto
 

+ 14 - 0
desktop/core/ext-py/phoenixdb/phoenixdb/avatica/proto/requests_pb2.py

@@ -1,3 +1,17 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
 # Generated by the protocol buffer compiler.  DO NOT EDIT!
 # source: requests.proto
 

+ 14 - 0
desktop/core/ext-py/phoenixdb/phoenixdb/avatica/proto/responses_pb2.py

@@ -1,3 +1,17 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
 # Generated by the protocol buffer compiler.  DO NOT EDIT!
 # source: responses.proto
 

+ 62 - 38
desktop/core/ext-py/phoenixdb/phoenixdb/connection.py

@@ -18,14 +18,17 @@ import uuid
 import weakref
 
 from phoenixdb import errors
-from phoenixdb.avatica.client import OPEN_CONNECTION_PROPERTIES
 from phoenixdb.cursor import Cursor
 from phoenixdb.errors import ProgrammingError
+from phoenixdb.meta import Meta
 
 __all__ = ['Connection']
 
 logger = logging.getLogger(__name__)
 
+AVATICA_PROPERTIES = ('autoCommit', 'autocommit', 'readOnly', 'readonly', 'transactionIsolation',
+                      'catalog', 'schema')
+
 
 class Connection(object):
     """Database connection.
@@ -46,17 +49,11 @@ class Connection(object):
         else:
             self.cursor_factory = Cursor
         self._cursors = []
-        # Extract properties to pass to OpenConnectionRequest
-        self._connection_args = {}
-        # The rest of the kwargs
-        self._filtered_args = {}
-        for k in kwargs:
-            if k in OPEN_CONNECTION_PROPERTIES:
-                self._connection_args[k] = kwargs[k]
-            else:
-                self._filtered_args[k] = kwargs[k]
+        self._phoenix_props, avatica_props_init = Connection._map_conn_props(kwargs)
         self.open()
-        self.set_session(**self._filtered_args)
+
+        # TODO we could probably optimize it away if the defaults are not changed
+        self.set_session(**avatica_props_init)
 
     def __del__(self):
         if not self._closed:
@@ -69,10 +66,36 @@ class Connection(object):
         if not self._closed:
             self.close()
 
+    @staticmethod
+    def _default_avatica_props():
+        return {'autoCommit': False,
+                'readOnly': False,
+                'transactionIsolation': 0,
+                'catalog': '',
+                'schema': ''}
+
+    @staticmethod
+    def _map_conn_props(conn_props):
+        """Sorts and prepocesses args that should be passed to Phoenix and Avatica"""
+
+        avatica_props = dict([(k, conn_props[k]) for k in conn_props.keys() if k in AVATICA_PROPERTIES])
+        phoenix_props = dict([(k, conn_props[k]) for k in conn_props.keys() if k not in AVATICA_PROPERTIES])
+        avatica_props = Connection._map_legacy_avatica_props(avatica_props)
+
+        return (phoenix_props, avatica_props)
+
+    @staticmethod
+    def _map_legacy_avatica_props(props):
+        if 'autocommit' in props:
+            props['autoCommit'] = bool(props.pop('autocommit'))
+        if 'readonly' in props:
+            props['readOnly'] = bool(props.pop('readonly'))
+        return props
+
     def open(self):
         """Opens the connection."""
         self._id = str(uuid.uuid4())
-        self._client.open_connection(self._id, info=self._connection_args)
+        self._client.open_connection(self._id, info=self._phoenix_props)
 
     def close(self):
         """Closes the connection.
@@ -83,7 +106,7 @@ class Connection(object):
         be automatically called at the end of the ``with`` block.
         """
         if self._closed:
-            raise ProgrammingError('the connection is already closed')
+            raise ProgrammingError('The connection is already closed.')
         for cursor_ref in self._cursors:
             cursor = cursor_ref()
             if cursor is not None and not cursor._closed:
@@ -99,12 +122,12 @@ class Connection(object):
 
     def commit(self):
         if self._closed:
-            raise ProgrammingError('the connection is already closed')
+            raise ProgrammingError('The connection is already closed.')
         self._client.commit(self._id)
 
     def rollback(self):
         if self._closed:
-            raise ProgrammingError('the connection is already closed')
+            raise ProgrammingError('The connection is already closed.')
         self._client.rollback(self._id)
 
     def cursor(self, cursor_factory=None):
@@ -121,12 +144,12 @@ class Connection(object):
             A :class:`~phoenixdb.cursor.Cursor` object.
         """
         if self._closed:
-            raise ProgrammingError('the connection is already closed')
+            raise ProgrammingError('The connection is already closed.')
         cursor = (cursor_factory or self.cursor_factory)(self)
         self._cursors.append(weakref.ref(cursor, self._cursors.remove))
         return cursor
 
-    def set_session(self, autocommit=None, readonly=None):
+    def set_session(self, **props):
         """Sets one or more parameters in the current connection.
 
         :param autocommit:
@@ -135,50 +158,51 @@ class Connection(object):
         :param readonly:
             Switch the connection to read-only mode.
         """
-        props = {}
-        if autocommit is not None:
-            props['autoCommit'] = bool(autocommit)
-        if readonly is not None:
-            props['readOnly'] = bool(readonly)
-        props = self._client.connection_sync(self._id, props)
-        self._autocommit = props.auto_commit
-        self._readonly = props.read_only
-        self._transactionisolation = props.transaction_isolation
+        props = Connection._map_legacy_avatica_props(props)
+        self._avatica_props = self._client.connection_sync_dict(self._id, props)
 
     @property
     def autocommit(self):
         """Read/write attribute for switching the connection's autocommit mode."""
-        return self._autocommit
+        return self._avatica_props['autoCommit']
 
     @autocommit.setter
     def autocommit(self, value):
         if self._closed:
-            raise ProgrammingError('the connection is already closed')
-        props = self._client.connection_sync(self._id, {'autoCommit': bool(value)})
-        self._autocommit = props.auto_commit
+            raise ProgrammingError('The connection is already closed.')
+        self._avatica_props = self._client.connection_sync_dict(self._id, {'autoCommit': bool(value)})
 
     @property
     def readonly(self):
         """Read/write attribute for switching the connection's readonly mode."""
-        return self._readonly
+        return self._avatica_props['readOnly']
 
     @readonly.setter
     def readonly(self, value):
         if self._closed:
-            raise ProgrammingError('the connection is already closed')
-        props = self._client.connection_sync(self._id, {'readOnly': bool(value)})
-        self._readonly = props.read_only
+            raise ProgrammingError('The connection is already closed.')
+        self._avatica_props = self._client.connection_sync_dict(self._id, {'readOnly': bool(value)})
 
     @property
     def transactionisolation(self):
-        return self._transactionisolation
+        return self._avatica_props['_transactionIsolation']
 
     @transactionisolation.setter
     def transactionisolation(self, value):
         if self._closed:
-            raise ProgrammingError('the connection is already closed')
-        props = self._client.connection_sync(self._id, {'transactionIsolation': bool(value)})
-        self._transactionisolation = props.transaction_isolation
+            raise ProgrammingError('The connection is already closed.')
+        self._avatica_props = self._client.connection_sync_dict(self._id, {'transactionIsolation': bool(value)})
+
+    def meta(self):
+        """Creates a new meta.
+
+        :returns:
+            A :class:`~phoenixdb.meta` object.
+        """
+        if self._closed:
+            raise ProgrammingError('The connection is already closed.')
+        meta = Meta(self)
+        return meta
 
 
 for name in errors.__all__:

+ 8 - 6
desktop/core/ext-py/phoenixdb/phoenixdb/cursor.py

@@ -162,14 +162,16 @@ class Cursor(object):
             offset=offset, frame_max_size=self.itersize)
         self._set_frame(frame)
 
+    def _process_result(self, result):
+        if result.own_statement:
+            self._set_id(result.statement_id)
+        self._set_signature(result.signature if result.HasField('signature') else None)
+        self._set_frame(result.first_frame if result.HasField('first_frame') else None)
+        self._updatecount = result.update_count
+
     def _process_results(self, results):
         if results:
-            result = results[0]
-            if result.own_statement:
-                self._set_id(result.statement_id)
-            self._set_signature(result.signature if result.HasField('signature') else None)
-            self._set_frame(result.first_frame if result.HasField('first_frame') else None)
-            self._updatecount = result.update_count
+            return self._process_result(results[0])
 
     def _transform_parameters(self, parameters):
         typed_parameters = []

+ 96 - 0
desktop/core/ext-py/phoenixdb/phoenixdb/meta.py

@@ -0,0 +1,96 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import sys
+import logging
+
+from phoenixdb.errors import ProgrammingError
+from phoenixdb.cursor import DictCursor
+
+
+__all__ = ['Meta']
+
+logger = logging.getLogger(__name__)
+
+
+class Meta(object):
+    """Database meta for querying MetaData
+    """
+
+    def __init__(self, connection):
+        self._connection = connection
+
+    def get_catalogs(self):
+        if self._connection._closed:
+            raise ProgrammingError('The connection is already closed.')
+        result = self._connection._client.get_catalogs(self._connection._id)
+        with DictCursor(self._connection) as cursor:
+            cursor._process_result(result)
+            return cursor.fetchall()
+
+    def get_schemas(self, catalog=None, schemaPattern=None):
+        if self._connection._closed:
+            raise ProgrammingError('The connection is already closed.')
+        result = self._connection._client.get_schemas(self._connection._id, catalog, schemaPattern)
+        with DictCursor(self._connection) as cursor:
+            cursor._process_result(result)
+            return self._fix_default(cursor.fetchall(), schemaPattern=schemaPattern)
+
+    def get_tables(self, catalog=None, schemaPattern=None, tableNamePattern=None, typeList=None):
+        if self._connection._closed:
+            raise ProgrammingError('The connection is already closed.')
+        result = self._connection._client.get_tables(
+            self._connection._id, catalog, schemaPattern, tableNamePattern, typeList=typeList)
+        with DictCursor(self._connection) as cursor:
+            cursor._process_result(result)
+            return self._fix_default(cursor.fetchall(), catalog, schemaPattern)
+
+    def get_columns(self, catalog=None, schemaPattern=None, tableNamePattern=None,
+                    columnNamePattern=None):
+        if self._connection._closed:
+            raise ProgrammingError('The connection is already closed.')
+        result = self._connection._client.get_columns(
+            self._connection._id, catalog, schemaPattern, tableNamePattern, columnNamePattern)
+        with DictCursor(self._connection) as cursor:
+            cursor._process_result(result)
+            return self._fix_default(cursor.fetchall(), catalog, schemaPattern)
+
+    def get_table_types(self):
+        if self._connection._closed:
+            raise ProgrammingError('The connection is already closed.')
+        result = self._connection._client.get_table_types(self._connection._id)
+        with DictCursor(self._connection) as cursor:
+            cursor._process_result(result)
+            return cursor.fetchall()
+
+    def get_type_info(self):
+        if self._connection._closed:
+            raise ProgrammingError('The connection is already closed.')
+        result = self._connection._client.get_type_info(self._connection._id)
+        with DictCursor(self._connection) as cursor:
+            cursor._process_result(result)
+            return cursor.fetchall()
+
+    def _fix_default(self, rows, catalog=None, schemaPattern=None):
+        '''Workaround for PHOENIX-6003'''
+        if schemaPattern == '':
+            rows = [row for row in rows if row['TABLE_SCHEM'] is None]
+        if catalog == '':
+            rows = [row for row in rows if row['TABLE_CATALOG'] is None]
+        # Couldn't find a sane way to do it that works on 2 and 3
+        if sys.version_info.major == 3:
+            return [{k: v or '' for k, v in row.items()} for row in rows]
+        else:
+            return [{k: v or '' for k, v in row.iteritems()} for row in rows]

+ 51 - 57
desktop/core/ext-py/phoenixdb/phoenixdb/sqlalchemy_phoenix.py

@@ -126,76 +126,70 @@ class PhoenixDialect(DefaultDialect):
         ))
         return [phoenix_url], connect_args
 
-    def has_table(self, connection, table_name, schema=None):
+    def has_table(self, connection, table_name, schema=None, **kw):
         if schema is None:
-            query = "SELECT 1 FROM system.catalog WHERE table_name = ? LIMIT 1"
-            params = [table_name.upper()]
-        else:
-            query = "SELECT 1 FROM system.catalog WHERE table_name = ? AND TABLE_SCHEM = ? LIMIT 1"
-            params = [table_name.upper(), schema.upper()]
-        return connection.execute(query, params).first() is not None
+            schema = ''
+        return bool(connection.connect().connection.meta().get_tables(
+            tableNamePattern=table_name,
+            schemaPattern=schema,
+            typeList=('TABLE', 'SYSTEM_TABLE')))
 
     def get_schema_names(self, connection, **kw):
-        query = "SELECT DISTINCT TABLE_SCHEM FROM SYSTEM.CATALOG"
-        return [row[0] for row in connection.execute(query)]
+        schemas = connection.connect().connection.meta().get_schemas()
+        schema_names = [schema['TABLE_SCHEM'] for schema in schemas]
+        # Phoenix won't return the default schema if there aren't any tables in it
+        if '' not in schema_names:
+            schema_names.insert(0, '')
+        return schema_names
+
+    def get_table_names(self, connection, schema=None, order_by=None, **kw):
+        '''order_by is ignored'''
+        if schema is None:
+            schema = ''
+        tables = connection.connect().connection.meta().get_tables(
+            schemaPattern=schema, typeList=('TABLE', 'SYSTEM TABLE'))
+        return [table['TABLE_NAME'] for table in tables]
 
-    def get_table_names(self, connection, schema=None, **kw):
+    def get_view_names(self, connection, schema=None, **kw):
         if schema is None:
-            query = "SELECT DISTINCT table_name FROM SYSTEM.CATALOG"
-            params = []
-        else:
-            query = "SELECT DISTINCT table_name FROM SYSTEM.CATALOG WHERE TABLE_SCHEM = ? "
-            params = [schema.upper()]
-        return [row[0] for row in connection.execute(query, params)]
+            schema = ''
+        return connection.connect().connection.meta().get_tables(schemaPattern=schema,
+                                                                 typeList=('VIEW'))
 
     def get_columns(self, connection, table_name, schema=None, **kw):
         if schema is None:
-            query = """SELECT COLUMN_NAME,  DATA_TYPE, NULLABLE
-                    FROM system.catalog
-                    WHERE table_name = ?
-                    AND ORDINAL_POSITION is not null
-                    ORDER BY ORDINAL_POSITION"""
-            params = [table_name.upper()]
-        else:
-            query = """SELECT COLUMN_NAME, DATA_TYPE, NULLABLE
-                    FROM system.catalog
-                    WHERE TABLE_SCHEM = ?
-                    AND table_name = ?
-                    AND ORDINAL_POSITION is not null
-                    ORDER BY ORDINAL_POSITION"""
-            params = [schema.upper(), table_name.upper()]
-
-        # get all of the fields for this table
-        c = connection.execute(query, params)
-        cols = []
-        while True:
-            row = c.fetchone()
-            if row is None:
-                break
-            name = row[0]
-            col_type = COLUMN_DATA_TYPE[row[1]]
-            nullable = row[2] == 1 if True else False
-
-            col_d = {
-                'name': name,
-                'type': col_type,
-                'nullable': nullable,
-                'default': None
-            }
-
-            cols.append(col_d)
-        return cols
-
-    # TODO This should be possible to implement
-    def get_pk_constraint(self, conn, table_name, schema=None, **kw):
+            schema = ''
+        raw = connection.connect().connection.meta().get_columns(
+            schemaPattern=schema, tableNamePattern=table_name)
+        return [self._map_column(row) for row in raw]
+
+    def get_pk_constraint(self, connection, table_name, schema=None, **kw):
+        if schema is None:
+            schema = ''
+        columns = connection.connect().connection.meta().get_columns(
+            schemaPattern=schema, tableNamePattern=table_name, *kw)
+        pk_columns = [col['COLUMN_NAME'] for col in columns if col['KEY_SEQ'] > 0]
+        return {'constrained_columns': pk_columns}
+
+    def get_indexes(self, conn, table_name, schema=None, **kw):
+        '''This information does not seem to be exposed via Avatica
+        TODO: Implement by directly querying SYSTEM tables ? '''
         return []
 
     def get_foreign_keys(self, conn, table_name, schema=None, **kw):
+        '''Foreign keys are a foreign concept to Phoenix,
+        but SqlAlchemy cannot parse the DB schema if it's not implemented '''
         return []
 
-    # TODO This should be possible to implement
-    def get_indexes(self, conn, table_name, schema=None, **kw):
-        return []
+    def _map_column(self, raw):
+        cooked = {}
+        cooked['name'] = raw['COLUMN_NAME']
+        cooked['type'] = COLUMN_DATA_TYPE[raw['TYPE_ID']]
+        cooked['nullable'] = bool(raw['IS_NULLABLE'])
+        cooked['autoincrement'] = bool(raw['IS_AUTOINCREMENT'])
+        cooked['comment'] = raw['REMARKS']
+        cooked['default'] = None  # Not apparent how to get this from the metatdata
+        return cooked
 
 
 class TINYINT(types.Integer):

+ 119 - 0
desktop/core/ext-py/phoenixdb/phoenixdb/tests/test_db.py

@@ -16,6 +16,7 @@
 import unittest
 
 import phoenixdb.cursor
+from phoenixdb.connection import Connection
 from phoenixdb.errors import InternalError
 from phoenixdb.tests import DatabaseTestCase, TEST_DB_URL
 
@@ -107,3 +108,121 @@ class PhoenixDatabaseTest(DatabaseTestCase):
             self.conn.autocommit = True
             cursor.execute("SELECT * FROM test ORDER BY id")
             self.assertEqual(cursor.fetchall(), [[1, 'one'], [2, 'two']])
+
+    def test_conn_props(self):
+        phoenix_args, avatica_args = Connection._map_conn_props(
+            {'autoCommit': True,
+             'readonly': True,
+             'transactionIsolation': 3,
+             'schema': 'bubu',
+             'phoenixArg': 'phoenixArg'})
+        self.assertEqual(phoenix_args, {'phoenixArg': 'phoenixArg'})
+        self.assertEqual(avatica_args, {'autoCommit': True,
+                                        'readOnly': True,
+                                        'transactionIsolation': 3,
+                                        'schema': 'bubu'})
+
+    def test_meta(self):
+        with self.conn.cursor() as cursor:
+            try:
+                cursor.execute('drop table if exists DEFAULT_TABLE')
+                cursor.execute('drop table if exists A_SCHEMA.A_TABLE')
+                cursor.execute('drop table if exists B_SCHMEA.B_TABLE')
+
+                cursor.execute('create table DEFAULT_TABLE (ID integer primary key)')
+                cursor.execute('create table A_SCHEMA.A_TABLE (ID_A integer primary key)')
+                cursor.execute('create table B_SCHEMA.B_TABLE (ID_B integer primary key)')
+
+                meta = self.conn.meta()
+
+                self.assertEqual(meta.get_catalogs(), [])
+
+                self.assertEqual(meta.get_schemas(), [
+                    {'TABLE_SCHEM': '', 'TABLE_CATALOG': ''},
+                    {'TABLE_SCHEM': 'A_SCHEMA', 'TABLE_CATALOG': ''},
+                    {'TABLE_SCHEM': 'B_SCHEMA', 'TABLE_CATALOG': ''},
+                    {'TABLE_SCHEM': 'SYSTEM', 'TABLE_CATALOG': ''}])
+
+                self.assertEqual(meta.get_schemas(schemaPattern=''), [
+                    {'TABLE_SCHEM': '', 'TABLE_CATALOG': ''}])
+
+                self.assertEqual(meta.get_schemas(schemaPattern='A_SCHEMA'), [
+                    {'TABLE_SCHEM': 'A_SCHEMA', 'TABLE_CATALOG': ''}])
+
+                a_tables = meta.get_tables()
+                self.assertTrue(len(a_tables) > 3)  # Don't know how many tables SYSTEM has
+
+                a_tables = meta.get_tables(schemaPattern='')
+                self.assertEqual(len(a_tables), 1)
+                self.assertTrue(a_tables[0]['TABLE_NAME'] == 'DEFAULT_TABLE')
+
+                a_tables = meta.get_tables(schemaPattern='A_SCHEMA')
+                self.assertEqual(len(a_tables), 1)
+                self.assertTrue(a_tables[0]['TABLE_NAME'] == 'A_TABLE')
+
+                a_columns = meta.get_columns(schemaPattern='A_SCHEMA', tableNamePattern='A_TABLE')
+                self.assertEqual(len(a_columns), 1)
+                self.assertTrue(a_columns[0]['COLUMN_NAME'] == 'ID_A')
+
+                self.assertTrue(all(elem in meta.get_table_types() for elem in [
+                    {'TABLE_TYPE': 'INDEX'},
+                    {'TABLE_TYPE': 'SEQUENCE'},
+                    {'TABLE_TYPE': 'SYSTEM TABLE'},
+                    {'TABLE_TYPE': 'TABLE'},
+                    {'TABLE_TYPE': 'VIEW'}]))
+
+                self.assertEqual(meta.get_type_info(), [])
+            finally:
+                cursor.execute('drop table if exists DEFAULT_TABLE')
+                cursor.execute('drop table if exists A_SCHEMA.A_TABLE')
+                cursor.execute('drop table if exists B_SCHEMA.B_TABLE')
+
+    @unittest.skip("https://issues.apache.org/jira/browse/PHOENIX-6004")
+    def test_case_sensitivity(self):
+        with self.conn.cursor() as cursor:
+            try:
+                cursor.execute('drop table if exists AAA')
+                cursor.execute('drop table if exists "aaa"')
+                cursor.execute('drop table if exists "Aaa"')
+
+                cursor.execute('create table AAA (ID integer primary key, YYY integer)')
+                cursor.execute('create table "aaa" ("ID_x" integer primary key, YYY integer, "Yyy" integer, "yyy" integer)')
+                cursor.execute('create table "Aaa" (ID_X integer primary key, ZZZ integer, "Zzz" integer, "zzz" integer)')
+
+                cursor.execute('upsert into AAA values (1, 2)')
+                cursor.execute('upsert into "aaa" values (11, 12, 13, 14)')
+                cursor.execute('upsert into "Aaa" values (21, 22, 23, 24)')
+
+                cursor.execute('select YYY from AAA')
+                self.assertEqual(cursor.fetchone(), [2])
+
+                cursor.execute('select YYY from "aaa"')
+                self.assertEqual(cursor.fetchone(), [12])
+
+                cursor.execute('select "YYY" from "aaa"')
+                self.assertEqual(cursor.fetchone(), [12])
+
+                cursor.execute('select "Yyy" from "aaa"')
+                self.assertEqual(cursor.fetchone(), [13])
+
+                meta = self.conn.meta()
+
+                self.assertEquals(len(meta.get_tables(schemaPattern='')), 3)
+
+                print(meta.get_columns(schemaPattern='',
+                                       tableNamePattern='"aaa"'))
+
+                self.assertEquals(len(meta.get_tables(schemaPattern='',
+                                                      tableNamePattern='AAA')), 1)
+                self.assertEquals(len(meta.get_tables(schemaPattern='',
+                                                      tableNamePattern='"aaa"')), 1)
+                self.assertEquals(meta.get_columns(tableNamePattern='AAA',
+                                                   columnNamePattern='YYY'), 1)
+                self.assertEquals(meta.get_columns(tableNamePattern='AAA',
+                                                   columnNamePattern='yyy'), 1)
+                self.assertEquals(meta.get_columns(tableNamePattern='AAA',
+                                                   columnNamePattern='"yyy"'), 0)
+            finally:
+                cursor.execute('drop table if exists AAA')
+                cursor.execute('drop table if exists "aaa"')
+                cursor.execute('drop table if exists "Aaa"')

+ 40 - 2
desktop/core/ext-py/phoenixdb/phoenixdb/tests/test_sqlalchemy.py

@@ -35,7 +35,7 @@ class SQLAlchemyTest(unittest.TestCase):
         engine = self._create_engine()
         # connection = engine.connect()
         metadata = db.MetaData()
-        catalog = db.Table('CATALOG', metadata, autoload=True, autoload_with=engine)
+        catalog = db.Table('CATALOG', metadata, schema='SYSTEM', autoload=True, autoload_with=engine)
         self.assertIn('TABLE_NAME', catalog.columns.keys())
 
     def test_textual(self):
@@ -52,6 +52,44 @@ class SQLAlchemyTest(unittest.TestCase):
             finally:
                 connection.execute('drop table if exists ALCHEMY_TEST')
 
+    def test_schema_filtering(self):
+        engine = self._create_engine()
+        with engine.connect() as connection:
+            try:
+                inspector = db.inspect(engine)
+
+                connection.execute('drop table if exists ALCHEMY_TEST')
+                connection.execute('drop table if exists A.ALCHEMY_TEST_A')
+                connection.execute('drop table if exists B.ALCHEMY_TEST_B')
+
+                self.assertEqual(inspector.get_schema_names(), ['', 'SYSTEM'])
+
+                connection.execute(text('create table ALCHEMY_TEST (ID integer primary key)'))
+                connection.execute(text('create table A.ALCHEMY_TEST_A (ID_A integer primary key)'))
+                connection.execute(text('create table B.ALCHEMY_TEST_B (ID_B integer primary key)'))
+
+                self.assertEqual(inspector.get_schema_names(), ['', 'A', 'B', 'SYSTEM'])
+
+                self.assertEqual(inspector.get_table_names(), ['ALCHEMY_TEST'])
+                self.assertEqual(inspector.get_table_names(''), ['ALCHEMY_TEST'])
+                self.assertEqual(inspector.get_table_names('A'), ['ALCHEMY_TEST_A'])
+                self.assertEqual(inspector.get_table_names('B'), ['ALCHEMY_TEST_B'])
+
+                self.assertEqual(inspector.get_columns('ALCHEMY_TEST').pop()['name'], 'ID')
+                self.assertEqual(
+                    inspector.get_columns('ALCHEMY_TEST', '').pop()['name'], 'ID')
+                self.assertEqual(
+                    inspector.get_columns('ALCHEMY_TEST_A', 'A').pop()['name'], 'ID_A')
+
+                self.assertTrue(engine.has_table('ALCHEMY_TEST'))
+                self.assertFalse(engine.has_table('ALCHEMY_TEST', 'A'))
+                self.assertTrue(engine.has_table('ALCHEMY_TEST_A', 'A'))
+                self.assertFalse(engine.has_table('ALCHEMY_TEST', 'A'))
+            finally:
+                connection.execute('drop table if exists ALCHEMY_TEST')
+                connection.execute('drop table if exists A.ALCHEMY_TEST_A')
+                connection.execute('drop table if exists B.ALCHEMY_TEST_B')
+
     def test_reflection(self):
         engine = self._create_engine()
         with engine.connect() as connection:
@@ -65,7 +103,7 @@ class SQLAlchemyTest(unittest.TestCase):
                 city VARCHAR NOT NULL,
                 population BIGINT
                 CONSTRAINT my_pk PRIMARY KEY (state, city))'''))
-                columns_result = inspector.get_columns('us_population')
+                columns_result = inspector.get_columns('US_POPULATION')
                 self.assertEqual(len(columns_result), 3)
             finally:
                 connection.execute('drop table if exists us_population')