Browse Source

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

SqlAlchemy schema filtering incorrect semantics
https://issues.apache.org/jira/browse/PHOENIX-5994
Romain 5 years ago
parent
commit
76bd01532c
32 changed files with 1278 additions and 153 deletions
  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
 from themattrix/tox-base
 
 
 RUN apt-get update && apt-get install -y krb5-user libkrb5-dev
 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_TRUSTSTORE=
 ENV PHOENIXDB_TEST_DB_AUTHENTICATION=
 ENV PHOENIXDB_TEST_DB_AUTHENTICATION=
 ENV PHOENIXDB_TEST_DB_AVATICA_USER=
 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
 from maven:3-jdk-8
 
 
 RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -yq krb5-user libkrb5-dev
 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
 # 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
 - Removed shell example, as it was python2 only
 - Updated documentation
 - Updated documentation
 - Added SQLAlchemy dialect
 - Added SQLAlchemy dialect
+- Implemented Avatica Metadata API
+- Misc fixes
+- Licensing cleanup
 
 
 Version 0.7
 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
 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
 using the
 `remote query server <http://phoenix.apache.org/server.html>`_.
 `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
 Installation
 ------------
 ------------
@@ -59,18 +60,18 @@ necessary requirements::
 You can start a Phoenix QueryServer instance on http://localhost:8765 for testing by running
 You can start a Phoenix QueryServer instance on http://localhost:8765 for testing by running
 the following command in the phoenix-queryserver directory::
 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 \
     -Dit.test=QueryServerBasicsIT\#startLocalPQS \
     -Ddo.not.randomize.pqs.port=true -Dstart.unsecure.pqs=true
     -Ddo.not.randomize.pqs.port=true -Dstart.unsecure.pqs=true
 
 
 You can start a secure (https+kerberos) Phoenix QueryServer instance on https://localhost:8765
 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::
 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 \
     -Dit.test=SecureQueryServerPhoenixDBIT\#startLocalPQS \
     -Ddo.not.randomize.pqs.port=true -Dstart.secure.pqs=true
     -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.
 up the environment for the tests.
 
 
 If you want to use the library without installing the phoenixdb library, you can use
 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 
 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,
 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
 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
     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)
   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>`_)
   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>`_)
 - 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
 FROM ubuntu:xenial
 
 
 RUN apt-get update && \
 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
 FROM openjdk:8
 
 
 ARG HBASE_VERSION
 ARG HBASE_VERSION

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

@@ -1,4 +1,19 @@
 #!/usr/bin/env bash
 #!/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=()
 pids=()
 
 

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

@@ -1,5 +1,22 @@
 <?xml version="1.0"?>
 <?xml version="1.0"?>
 <?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
 <?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>
 <configuration>
     <property>
     <property>
         <name>hbase.regionserver.wal.codec</name>
         <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
 # 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
 API Reference
 =============
 =============
 
 

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

@@ -1,5 +1,20 @@
 # -*- coding: utf-8 -*-
 # -*- 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
 # phoenixdb documentation build configuration file, created by
 # sphinx-quickstart on Sun Jun 28 18:07:35 2015.
 # 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
 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
 .. 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
   sed -i 's/import common_pb2/from . import common_pb2/' phoenixdb/avatica/proto/*_pb2.py
 fi
 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
 rm -rf avatica-tmp

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

@@ -1,10 +1,13 @@
 # Copyright 2015 Lukas Lalinsky
 # 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
 # Unless required by applicable law or agreed to in writing, software
 # distributed under the License is distributed on an "AS IS" BASIS,
 # 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
 # See the License for the specific language governing permissions and
 # limitations under the License.
 # 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 logging
 import math
 import math
@@ -85,24 +88,12 @@ SQLSTATE_ERROR_CLASSES = [
     ('INT', errors.InternalError),  # Phoenix internal error
     ('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):
 def raise_sql_error(code, sqlstate, message):
     for prefix, error_class in SQLSTATE_ERROR_CLASSES:
     for prefix, error_class in SQLSTATE_ERROR_CLASSES:
         if sqlstate.startswith(prefix):
         if sqlstate.startswith(prefix):
             raise error_class(message, code, sqlstate)
             raise error_class(message, code, sqlstate)
+    raise errors.InternalError(message, code, sqlstate)
 
 
 
 
 def parse_and_raise_sql_error(message):
 def parse_and_raise_sql_error(message):
@@ -122,15 +113,20 @@ def parse_error_page(html):
 
 
 
 
 def parse_error_protobuf(text):
 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)
     parse_and_raise_sql_error(err.error_message)
     raise_sql_error(err.error_code, err.sql_state, 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):
 class AvaticaClient(object):
@@ -236,7 +232,10 @@ class AvaticaClient(object):
     def get_catalogs(self, connection_id):
     def get_catalogs(self, connection_id):
         request = requests_pb2.CatalogsRequest()
         request = requests_pb2.CatalogsRequest()
         request.connection_id = connection_id
         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):
     def get_schemas(self, connection_id, catalog=None, schemaPattern=None):
         request = requests_pb2.SchemasRequest()
         request = requests_pb2.SchemasRequest()
@@ -245,7 +244,10 @@ class AvaticaClient(object):
             request.catalog = catalog
             request.catalog = catalog
         if schemaPattern is not None:
         if schemaPattern is not None:
             request.schema_pattern = schemaPattern
             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):
     def get_tables(self, connection_id, catalog=None, schemaPattern=None, tableNamePattern=None, typeList=None):
         request = requests_pb2.TablesRequest()
         request = requests_pb2.TablesRequest()
@@ -256,12 +258,13 @@ class AvaticaClient(object):
             request.schema_pattern = schemaPattern
             request.schema_pattern = schemaPattern
         if tableNamePattern is not None:
         if tableNamePattern is not None:
             request.table_name_pattern = tableNamePattern
             request.table_name_pattern = tableNamePattern
-        if typeList is not None:
-            request.type_list = typeList
         if typeList is not None:
         if typeList is not None:
             request.type_list.extend(typeList)
             request.type_list.extend(typeList)
         request.has_type_list = typeList is not None
         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):
     def get_columns(self, connection_id, catalog=None, schemaPattern=None, tableNamePattern=None, columnNamePattern=None):
         request = requests_pb2.ColumnsRequest()
         request = requests_pb2.ColumnsRequest()
@@ -274,17 +277,35 @@ class AvaticaClient(object):
             request.table_name_pattern = tableNamePattern
             request.table_name_pattern = tableNamePattern
         if columnNamePattern is not None:
         if columnNamePattern is not None:
             request.column_name_pattern = columnNamePattern
             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):
     def get_table_types(self, connection_id):
         request = requests_pb2.TableTypesRequest()
         request = requests_pb2.TableTypesRequest()
         request.connection_id = connection_id
         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):
     def get_type_info(self, connection_id):
         request = requests_pb2.TypeInfoRequest()
         request = requests_pb2.TypeInfoRequest()
         request.connection_id = connection_id
         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):
     def connection_sync(self, connection_id, connProps=None):
         """Synchronizes connection properties with the server.
         """Synchronizes connection properties with the server.
@@ -298,18 +319,28 @@ class AvaticaClient(object):
         :returns:
         :returns:
             A ``common_pb2.ConnectionProperties`` object.
             A ``common_pb2.ConnectionProperties`` object.
         """
         """
-        if connProps is None:
-            connProps = {}
+        if connProps:
+            props = connProps.copy()
+        else:
+            props = {}
 
 
         request = requests_pb2.ConnectionSyncRequest()
         request = requests_pb2.ConnectionSyncRequest()
         request.connection_id = connection_id
         request.connection_id = connection_id
-        request.conn_props.auto_commit = connProps.get('autoCommit', False)
         request.conn_props.has_auto_commit = True
         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.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_data = self._apply(request)
         response = responses_pb2.ConnectionSyncResponse()
         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!
 # Generated by the protocol buffer compiler.  DO NOT EDIT!
 # source: common.proto
 # 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!
 # Generated by the protocol buffer compiler.  DO NOT EDIT!
 # source: requests.proto
 # 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!
 # Generated by the protocol buffer compiler.  DO NOT EDIT!
 # source: responses.proto
 # source: responses.proto
 
 

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

@@ -18,14 +18,17 @@ import uuid
 import weakref
 import weakref
 
 
 from phoenixdb import errors
 from phoenixdb import errors
-from phoenixdb.avatica.client import OPEN_CONNECTION_PROPERTIES
 from phoenixdb.cursor import Cursor
 from phoenixdb.cursor import Cursor
 from phoenixdb.errors import ProgrammingError
 from phoenixdb.errors import ProgrammingError
+from phoenixdb.meta import Meta
 
 
 __all__ = ['Connection']
 __all__ = ['Connection']
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
+AVATICA_PROPERTIES = ('autoCommit', 'autocommit', 'readOnly', 'readonly', 'transactionIsolation',
+                      'catalog', 'schema')
+
 
 
 class Connection(object):
 class Connection(object):
     """Database connection.
     """Database connection.
@@ -46,17 +49,11 @@ class Connection(object):
         else:
         else:
             self.cursor_factory = Cursor
             self.cursor_factory = Cursor
         self._cursors = []
         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.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):
     def __del__(self):
         if not self._closed:
         if not self._closed:
@@ -69,10 +66,36 @@ class Connection(object):
         if not self._closed:
         if not self._closed:
             self.close()
             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):
     def open(self):
         """Opens the connection."""
         """Opens the connection."""
         self._id = str(uuid.uuid4())
         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):
     def close(self):
         """Closes the connection.
         """Closes the connection.
@@ -83,7 +106,7 @@ class Connection(object):
         be automatically called at the end of the ``with`` block.
         be automatically called at the end of the ``with`` block.
         """
         """
         if self._closed:
         if self._closed:
-            raise ProgrammingError('the connection is already closed')
+            raise ProgrammingError('The connection is already closed.')
         for cursor_ref in self._cursors:
         for cursor_ref in self._cursors:
             cursor = cursor_ref()
             cursor = cursor_ref()
             if cursor is not None and not cursor._closed:
             if cursor is not None and not cursor._closed:
@@ -99,12 +122,12 @@ class Connection(object):
 
 
     def commit(self):
     def commit(self):
         if self._closed:
         if self._closed:
-            raise ProgrammingError('the connection is already closed')
+            raise ProgrammingError('The connection is already closed.')
         self._client.commit(self._id)
         self._client.commit(self._id)
 
 
     def rollback(self):
     def rollback(self):
         if self._closed:
         if self._closed:
-            raise ProgrammingError('the connection is already closed')
+            raise ProgrammingError('The connection is already closed.')
         self._client.rollback(self._id)
         self._client.rollback(self._id)
 
 
     def cursor(self, cursor_factory=None):
     def cursor(self, cursor_factory=None):
@@ -121,12 +144,12 @@ class Connection(object):
             A :class:`~phoenixdb.cursor.Cursor` object.
             A :class:`~phoenixdb.cursor.Cursor` object.
         """
         """
         if self._closed:
         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)
         cursor = (cursor_factory or self.cursor_factory)(self)
         self._cursors.append(weakref.ref(cursor, self._cursors.remove))
         self._cursors.append(weakref.ref(cursor, self._cursors.remove))
         return cursor
         return cursor
 
 
-    def set_session(self, autocommit=None, readonly=None):
+    def set_session(self, **props):
         """Sets one or more parameters in the current connection.
         """Sets one or more parameters in the current connection.
 
 
         :param autocommit:
         :param autocommit:
@@ -135,50 +158,51 @@ class Connection(object):
         :param readonly:
         :param readonly:
             Switch the connection to read-only mode.
             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
     @property
     def autocommit(self):
     def autocommit(self):
         """Read/write attribute for switching the connection's autocommit mode."""
         """Read/write attribute for switching the connection's autocommit mode."""
-        return self._autocommit
+        return self._avatica_props['autoCommit']
 
 
     @autocommit.setter
     @autocommit.setter
     def autocommit(self, value):
     def autocommit(self, value):
         if self._closed:
         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
     @property
     def readonly(self):
     def readonly(self):
         """Read/write attribute for switching the connection's readonly mode."""
         """Read/write attribute for switching the connection's readonly mode."""
-        return self._readonly
+        return self._avatica_props['readOnly']
 
 
     @readonly.setter
     @readonly.setter
     def readonly(self, value):
     def readonly(self, value):
         if self._closed:
         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
     @property
     def transactionisolation(self):
     def transactionisolation(self):
-        return self._transactionisolation
+        return self._avatica_props['_transactionIsolation']
 
 
     @transactionisolation.setter
     @transactionisolation.setter
     def transactionisolation(self, value):
     def transactionisolation(self, value):
         if self._closed:
         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__:
 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)
             offset=offset, frame_max_size=self.itersize)
         self._set_frame(frame)
         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):
     def _process_results(self, results):
         if 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):
     def _transform_parameters(self, parameters):
         typed_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
         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:
         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):
     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:
         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):
     def get_columns(self, connection, table_name, schema=None, **kw):
         if schema is None:
         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 []
         return []
 
 
     def get_foreign_keys(self, conn, table_name, schema=None, **kw):
     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 []
         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):
 class TINYINT(types.Integer):

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

@@ -16,6 +16,7 @@
 import unittest
 import unittest
 
 
 import phoenixdb.cursor
 import phoenixdb.cursor
+from phoenixdb.connection import Connection
 from phoenixdb.errors import InternalError
 from phoenixdb.errors import InternalError
 from phoenixdb.tests import DatabaseTestCase, TEST_DB_URL
 from phoenixdb.tests import DatabaseTestCase, TEST_DB_URL
 
 
@@ -107,3 +108,121 @@ class PhoenixDatabaseTest(DatabaseTestCase):
             self.conn.autocommit = True
             self.conn.autocommit = True
             cursor.execute("SELECT * FROM test ORDER BY id")
             cursor.execute("SELECT * FROM test ORDER BY id")
             self.assertEqual(cursor.fetchall(), [[1, 'one'], [2, 'two']])
             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()
         engine = self._create_engine()
         # connection = engine.connect()
         # connection = engine.connect()
         metadata = db.MetaData()
         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())
         self.assertIn('TABLE_NAME', catalog.columns.keys())
 
 
     def test_textual(self):
     def test_textual(self):
@@ -52,6 +52,44 @@ class SQLAlchemyTest(unittest.TestCase):
             finally:
             finally:
                 connection.execute('drop table if exists ALCHEMY_TEST')
                 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):
     def test_reflection(self):
         engine = self._create_engine()
         engine = self._create_engine()
         with engine.connect() as connection:
         with engine.connect() as connection:
@@ -65,7 +103,7 @@ class SQLAlchemyTest(unittest.TestCase):
                 city VARCHAR NOT NULL,
                 city VARCHAR NOT NULL,
                 population BIGINT
                 population BIGINT
                 CONSTRAINT my_pk PRIMARY KEY (state, city))'''))
                 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)
                 self.assertEqual(len(columns_result), 3)
             finally:
             finally:
                 connection.execute('drop table if exists us_population')
                 connection.execute('drop table if exists us_population')