├── .circleci └── config.yml ├── .coveragerc ├── .flake8 ├── .gitattributes ├── .gitignore ├── .gitlab-ci.yml ├── .pylint-rc ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── epydoc.css ├── pynuodb ├── __init__.py ├── connection.py ├── crypt.py ├── cursor.py ├── datatype.py ├── encodedsession.py ├── exception.py ├── protocol.py ├── result_set.py ├── session.py └── statement.py ├── requirements.txt ├── setup.py ├── test-performance └── timesInsert.py ├── test_requirements.txt └── tests ├── 640px-Starling.JPG ├── __init__.py ├── conftest.py ├── dbapi20.py ├── dbapi20_tpc.py ├── holmes.txt ├── mock_tzs.py ├── nuodb_base.py ├── nuodb_basic_test.py ├── nuodb_blob_test.py ├── nuodb_connect_test.py ├── nuodb_crypt_test.py ├── nuodb_cursor_test.py ├── nuodb_dbapi20_test.py ├── nuodb_description_name_test.py ├── nuodb_executionflow_test.py ├── nuodb_globals_test.py ├── nuodb_huge_test.py ├── nuodb_service_test.py ├── nuodb_statement_management_test.py ├── nuodb_transaction_test.py ├── nuodb_types_test.py └── sample.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | commands: 4 | after_failure: 5 | description: "Steps to be run after build or test failure" 6 | parameters: 7 | when: 8 | type: string 9 | steps: 10 | - run: 11 | name: On failure action 12 | command: | 13 | env | sort > artifacts/env.out 14 | cp -a $HOME/.bashrc artifacts || true 15 | cp -a $HOME/.profile artifacts || true 16 | cp -a $HOME/.bash_profile artifacts || 17 | cp -a $HOME/.nuocmdrc artifacts || true 18 | cp -a /etc/nuodb/nuoadmin.conf artifacts || true 19 | cp -a /var/log/nuodb/nuoadmin.log* artifacts || true 20 | when: <> 21 | 22 | - when: 23 | condition: <> 24 | steps: 25 | - store_artifacts: 26 | path: artifacts 27 | 28 | jobs: 29 | build_n_run: 30 | description: "Build the nuodb-python module and run the test suite" 31 | docker: 32 | - image: nuodb/nuodb:latest 33 | user: root 34 | resource_class: small 35 | environment: 36 | TZ : America/New_York 37 | NUO_SET_TLS : disable 38 | NUOCMD_CLIENT_KEY : "" 39 | NUOCMD_VERIFY_SERVER : "" 40 | NUOCMD_PLUGINS : "" 41 | steps: 42 | - checkout 43 | - run: 44 | name: Make test directories 45 | command: mkdir -p artifacts results 46 | - run: 47 | name: Install make 48 | command: dnf install make -y 49 | - run: 50 | name: Install Python dependencies 51 | command: make install 52 | - run: 53 | name: Start NuoDB Admin 54 | command: | 55 | sudo -u nuodb /opt/nuodb/etc/nuoadmin tls $NUO_SET_TLS 56 | sudo -u nuodb /opt/nuodb/etc/nuoadmin tls status 57 | sudo -u nuodb /opt/nuodb/etc/nuoadmin start 58 | sudo -u nuodb /opt/nuodb/bin/nuocmd --show-json get effective-license 59 | - run: 60 | name: Run test 61 | command: make fulltest 62 | - store_artifacts: 63 | path: artifacts 64 | - store_test_results: 65 | path: results 66 | - after_failure: 67 | when : "on_fail" 68 | 69 | workflows: 70 | build-project: 71 | jobs: 72 | - build_n_run: 73 | name: "Build and run regression tests" 74 | context: 75 | - common-config 76 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | include=pynuodb/* 3 | 4 | [report] 5 | omit=tests/* 6 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 100 3 | max-complexity = 20 4 | ignore = 5 | # Allow multiple spaces before operators 6 | E221 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Default 2 | * text=auto 3 | 4 | # POSIX files 5 | *.sh eol=lf 6 | *.initd eol=lf 7 | 8 | # Windows files 9 | *.bat eol=crlf 10 | *.cmd eol=crlf 11 | *.sln eol=crlf 12 | *.reg eol=crlf 13 | *.hm eol=crlf 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | \#*\# 3 | 4 | *.diff 5 | *.patch 6 | *.orig 7 | 8 | *.py[cod] 9 | 10 | *.so 11 | 12 | /.virttemp 13 | /.testtemp 14 | 15 | # Packages 16 | *.egg 17 | *.egg-info 18 | dist 19 | build 20 | eggs 21 | parts 22 | bin 23 | var 24 | sdist 25 | develop-eggs 26 | .installed.cfg 27 | lib 28 | lib64 29 | 30 | # Installer logs 31 | pip-log.txt 32 | 33 | # Unit test / coverage reports 34 | .coverage 35 | .tox 36 | .testtemp 37 | nosetests.xml 38 | .mypy_cache/ 39 | artifacts/ 40 | results/ 41 | 42 | # Translations 43 | *.mo 44 | 45 | # Mr Developer 46 | .mr.developer.cfg 47 | .project 48 | .pydevproject 49 | .settings 50 | .idea 51 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # Enable gemnasium dependency vulnerability scanning 2 | dependency_scanning: 3 | image: docker:stable 4 | variables: 5 | DOCKER_DRIVER: overlay2 6 | allow_failure: true 7 | services: 8 | - docker:stable-dind 9 | script: 10 | - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/') 11 | - docker run 12 | --env DEP_SCAN_DISABLE_REMOTE_CHECKS="${DEP_SCAN_DISABLE_REMOTE_CHECKS:-false}" 13 | --volume "$PWD:/code" 14 | --volume /var/run/docker.sock:/var/run/docker.sock 15 | "registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$SP_VERSION" /code 16 | artifacts: 17 | paths: [gl-dependency-scanning-report.json] 18 | -------------------------------------------------------------------------------- /.pylint-rc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code 6 | extension-pkg-whitelist= 7 | 8 | # Add files or directories to the blacklist. They should be base names, not 9 | # paths. 10 | ignore=CVS 11 | 12 | # Add files or directories matching the regex patterns to the blacklist. The 13 | # regex matches against base names, not paths. 14 | ignore-patterns= 15 | 16 | # Python code to execute, usually for sys.path manipulation such as 17 | # pygtk.require(). 18 | #init-hook= 19 | 20 | # Use multiple processes to speed up Pylint. 21 | #jobs=1 22 | 23 | # List of plugins (as comma separated values of python modules names) to load, 24 | # usually to register additional checkers. 25 | load-plugins= 26 | 27 | # Pickle collected data for later comparisons. 28 | persistent=yes 29 | 30 | # Specify a configuration file. 31 | #rcfile= 32 | 33 | # When enabled, pylint would attempt to guess common misconfiguration and emit 34 | # user-friendly hints instead of false-positive error messages 35 | suggestion-mode=yes 36 | 37 | # Allow loading of arbitrary C extensions. Extensions are imported into the 38 | # active Python interpreter and may run arbitrary code. 39 | unsafe-load-any-extension=no 40 | 41 | 42 | [MESSAGES CONTROL] 43 | 44 | # Only show warnings with the listed confidence levels. Leave empty to show 45 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 46 | confidence= 47 | 48 | # Disable the message, report, category or checker with the given id(s). You 49 | # can either give multiple identifiers separated by comma (,) or put this 50 | # option multiple times (only on the command line, not in the configuration 51 | # file where it should appear only once).You can also use "--disable=all" to 52 | # disable everything first and then reenable specific checks. For example, if 53 | # you want to run only the similarities checker, you can use "--disable=all 54 | # --enable=similarities". If you want to run only the classes checker, but have 55 | # no Warning level messages displayed, use"--disable=all --enable=classes 56 | # --disable=W" 57 | disable=print-statement, 58 | parameter-unpacking, 59 | unpacking-in-except, 60 | old-raise-syntax, 61 | backtick, 62 | long-suffix, 63 | old-ne-operator, 64 | old-octal-literal, 65 | import-star-module-level, 66 | non-ascii-bytes-literal, 67 | invalid-unicode-literal, 68 | raw-checker-failed, 69 | bad-inline-option, 70 | locally-disabled, 71 | locally-enabled, 72 | file-ignored, 73 | suppressed-message, 74 | useless-suppression, 75 | deprecated-pragma, 76 | apply-builtin, 77 | basestring-builtin, 78 | buffer-builtin, 79 | cmp-builtin, 80 | coerce-builtin, 81 | execfile-builtin, 82 | file-builtin, 83 | long-builtin, 84 | raw_input-builtin, 85 | reduce-builtin, 86 | standarderror-builtin, 87 | unicode-builtin, 88 | xrange-builtin, 89 | coerce-method, 90 | delslice-method, 91 | getslice-method, 92 | setslice-method, 93 | no-absolute-import, 94 | old-division, 95 | dict-iter-method, 96 | dict-view-method, 97 | next-method-called, 98 | metaclass-assignment, 99 | indexing-exception, 100 | raising-string, 101 | reload-builtin, 102 | oct-method, 103 | hex-method, 104 | nonzero-method, 105 | cmp-method, 106 | input-builtin, 107 | round-builtin, 108 | intern-builtin, 109 | unichr-builtin, 110 | map-builtin-not-iterating, 111 | zip-builtin-not-iterating, 112 | range-builtin-not-iterating, 113 | filter-builtin-not-iterating, 114 | using-cmp-argument, 115 | eq-without-hash, 116 | div-method, 117 | idiv-method, 118 | rdiv-method, 119 | exception-message-attribute, 120 | invalid-str-codec, 121 | sys-max-int, 122 | bad-python3-import, 123 | deprecated-string-function, 124 | deprecated-str-translate-call, 125 | deprecated-itertools-function, 126 | deprecated-types-field, 127 | next-method-defined, 128 | dict-items-not-iterating, 129 | dict-keys-not-iterating, 130 | dict-values-not-iterating, 131 | deprecated-operator-function, 132 | deprecated-urllib-function, 133 | xreadlines-attribute, 134 | deprecated-sys-function, 135 | exception-escape, 136 | comprehension-escape, 137 | # Removed in latest pylint and fights with flake8 etc. 138 | bad-continuation, 139 | # Added for pynuodb 140 | invalid-name 141 | 142 | # Enable the message, report, category or checker with the given id(s). You can 143 | # either give multiple identifier separated by comma (,) or put this option 144 | # multiple time (only on the command line, not in the configuration file where 145 | # it should appear only once). See also the "--disable" option for examples. 146 | enable=c-extension-no-member 147 | 148 | 149 | [REPORTS] 150 | 151 | # Python expression which should return a note less than 10 (10 is the highest 152 | # note). You have access to the variables errors warning, statement which 153 | # respectively contain the number of errors / warnings messages and the total 154 | # number of statements analyzed. This is used by the global evaluation report 155 | # (RP0004). 156 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 157 | 158 | # Template used to display messages. This is a python new-style format string 159 | # used to format the message information. See doc for all details 160 | #msg-template= 161 | 162 | # Set the output format. Available formats are text, parseable, colorized, json 163 | # and msvs (visual studio).You can also give a reporter class, eg 164 | # mypackage.mymodule.MyReporterClass. 165 | output-format=text 166 | 167 | # Tells whether to display a full report or only the messages 168 | reports=no 169 | 170 | # Activate the evaluation score. 171 | score=yes 172 | 173 | 174 | [REFACTORING] 175 | 176 | # Maximum number of nested blocks for function / method body 177 | max-nested-blocks=5 178 | 179 | # Complete name of functions that never returns. When checking for 180 | # inconsistent-return-statements if a never returning function is called then 181 | # it will be considered as an explicit return statement and no message will be 182 | # printed. 183 | never-returning-functions=optparse.Values,sys.exit 184 | 185 | 186 | [TYPECHECK] 187 | 188 | # List of decorators that produce context managers, such as 189 | # contextlib.contextmanager. Add to this list to register other decorators that 190 | # produce valid context managers. 191 | contextmanager-decorators=contextlib.contextmanager 192 | 193 | # List of members which are set dynamically and missed by pylint inference 194 | # system, and so shouldn't trigger E1101 when accessed. Python regular 195 | # expressions are accepted. 196 | generated-members= 197 | 198 | # Tells whether missing members accessed in mixin class should be ignored. A 199 | # mixin class is detected if its name ends with "mixin" (case insensitive). 200 | ignore-mixin-members=yes 201 | 202 | # This flag controls whether pylint should warn about no-member and similar 203 | # checks whenever an opaque object is returned when inferring. The inference 204 | # can return multiple potential results while evaluating a Python object, but 205 | # some branches might not be evaluated, which results in partial inference. In 206 | # that case, it might be useful to still emit no-member and other checks for 207 | # the rest of the inferred objects. 208 | ignore-on-opaque-inference=yes 209 | 210 | # List of class names for which member attributes should not be checked (useful 211 | # for classes with dynamically set attributes). This supports the use of 212 | # qualified names. 213 | ignored-classes=optparse.Values,thread._local,_thread._local 214 | 215 | # List of module names for which member attributes should not be checked 216 | # (useful for modules/projects where namespaces are manipulated during runtime 217 | # and thus existing member attributes cannot be deduced by static analysis. It 218 | # supports qualified module names, as well as Unix pattern matching. 219 | ignored-modules= 220 | 221 | # Show a hint with possible names when a member name was not found. The aspect 222 | # of finding the hint is based on edit distance. 223 | missing-member-hint=yes 224 | 225 | # The minimum edit distance a name should have in order to be considered a 226 | # similar match for a missing member name. 227 | missing-member-hint-distance=1 228 | 229 | # The total number of similar names that should be taken in consideration when 230 | # showing a hint for a missing member. 231 | missing-member-max-choices=1 232 | 233 | 234 | [MISCELLANEOUS] 235 | 236 | # List of note tags to take in consideration, separated by a comma. 237 | notes=FIXME, 238 | XXX, 239 | TODO 240 | 241 | 242 | [VARIABLES] 243 | 244 | # List of additional names supposed to be defined in builtins. Remember that 245 | # you should avoid to define new builtins when possible. 246 | additional-builtins= 247 | 248 | # Tells whether unused global variables should be treated as a violation. 249 | allow-global-unused-variables=yes 250 | 251 | # List of strings which can identify a callback function by name. A callback 252 | # name must start or end with one of those strings. 253 | callbacks=cb_, 254 | _cb 255 | 256 | # A regular expression matching the name of dummy variables (i.e. expectedly 257 | # not used). 258 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 259 | 260 | # Argument names that match this expression will be ignored. Default to name 261 | # with leading underscore 262 | ignored-argument-names=_.*|^ignored_|^unused_ 263 | 264 | # Tells whether we should check for unused import in __init__ files. 265 | init-import=no 266 | 267 | # List of qualified module names which can have objects that can redefine 268 | # builtins. 269 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,io,builtins 270 | 271 | 272 | [SPELLING] 273 | 274 | # Limits count of emitted suggestions for spelling mistakes 275 | max-spelling-suggestions=4 276 | 277 | # Spelling dictionary name. Available dictionaries: none. To make it working 278 | # install python-enchant package. 279 | spelling-dict= 280 | 281 | # List of comma separated words that should not be checked. 282 | spelling-ignore-words= 283 | 284 | # A path to a file that contains private dictionary; one word per line. 285 | spelling-private-dict-file= 286 | 287 | # Tells whether to store unknown words to indicated private dictionary in 288 | # --spelling-private-dict-file option instead of raising a message. 289 | spelling-store-unknown-words=no 290 | 291 | 292 | [SIMILARITIES] 293 | 294 | # Ignore comments when computing similarities. 295 | ignore-comments=yes 296 | 297 | # Ignore docstrings when computing similarities. 298 | ignore-docstrings=yes 299 | 300 | # Ignore imports when computing similarities. 301 | ignore-imports=no 302 | 303 | # Minimum lines number of a similarity. 304 | min-similarity-lines=4 305 | 306 | 307 | [FORMAT] 308 | 309 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 310 | expected-line-ending-format= 311 | 312 | # Regexp for a line that is allowed to be longer than the limit. 313 | ignore-long-lines=^\s*(# )??$ 314 | 315 | # Number of spaces of indent required inside a hanging or continued line. 316 | indent-after-paren=4 317 | 318 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 319 | # tab). 320 | indent-string=' ' 321 | 322 | # Maximum number of characters on a single line. 323 | max-line-length=100 324 | 325 | # Maximum number of lines in a module 326 | max-module-lines=1250 327 | 328 | # List of optional constructs for which whitespace checking is disabled. `dict- 329 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 330 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 331 | # `empty-line` allows space-only lines. 332 | no-space-check=trailing-comma, 333 | dict-separator 334 | 335 | # Allow the body of a class to be on the same line as the declaration if body 336 | # contains single statement. 337 | single-line-class-stmt=no 338 | 339 | # Allow the body of an if to be on the same line as the test if there is no 340 | # else. 341 | single-line-if-stmt=no 342 | 343 | 344 | [BASIC] 345 | 346 | # Naming style matching correct argument names 347 | argument-naming-style=snake_case 348 | 349 | # Regular expression matching correct argument names. Overrides argument- 350 | # naming-style 351 | #argument-rgx= 352 | 353 | # Naming style matching correct attribute names 354 | attr-naming-style=snake_case 355 | 356 | # Regular expression matching correct attribute names. Overrides attr-naming- 357 | # style 358 | #attr-rgx= 359 | 360 | # Bad variable names which should always be refused, separated by a comma 361 | bad-names=foo, 362 | bar, 363 | baz, 364 | toto, 365 | tutu, 366 | tata 367 | 368 | # Naming style matching correct class attribute names 369 | class-attribute-naming-style=any 370 | 371 | # Regular expression matching correct class attribute names. Overrides class- 372 | # attribute-naming-style 373 | #class-attribute-rgx= 374 | 375 | # Naming style matching correct class names 376 | class-naming-style=PascalCase 377 | 378 | # Regular expression matching correct class names. Overrides class-naming-style 379 | #class-rgx= 380 | 381 | # Naming style matching correct constant names 382 | const-naming-style=UPPER_CASE 383 | 384 | # Regular expression matching correct constant names. Overrides const-naming- 385 | # style 386 | #const-rgx= 387 | 388 | # Minimum line length for functions/classes that require docstrings, shorter 389 | # ones are exempt. 390 | docstring-min-length=-1 391 | 392 | # Naming style matching correct function names 393 | function-naming-style=snake_case 394 | 395 | # Regular expression matching correct function names. Overrides function- 396 | # naming-style 397 | #function-rgx= 398 | 399 | # Good variable names which should always be accepted, separated by a comma 400 | good-names=i, 401 | j, 402 | k, 403 | ex, 404 | Run, 405 | _ 406 | 407 | # Include a hint for the correct naming format with invalid-name 408 | include-naming-hint=no 409 | 410 | # Naming style matching correct inline iteration names 411 | inlinevar-naming-style=any 412 | 413 | # Regular expression matching correct inline iteration names. Overrides 414 | # inlinevar-naming-style 415 | #inlinevar-rgx= 416 | 417 | # Naming style matching correct method names 418 | method-naming-style=snake_case 419 | 420 | # Regular expression matching correct method names. Overrides method-naming- 421 | # style 422 | #method-rgx= 423 | 424 | # Naming style matching correct module names 425 | module-naming-style=snake_case 426 | 427 | # Regular expression matching correct module names. Overrides module-naming- 428 | # style 429 | #module-rgx= 430 | 431 | # Colon-delimited sets of names that determine each other's naming style when 432 | # the name regexes allow several styles. 433 | name-group= 434 | 435 | # Regular expression which should only match function or class names that do 436 | # not require a docstring. 437 | no-docstring-rgx=^_ 438 | 439 | # List of decorators that produce properties, such as abc.abstractproperty. Add 440 | # to this list to register other decorators that produce valid properties. 441 | property-classes=abc.abstractproperty 442 | 443 | # Naming style matching correct variable names 444 | variable-naming-style=snake_case 445 | 446 | # Regular expression matching correct variable names. Overrides variable- 447 | # naming-style 448 | #variable-rgx= 449 | 450 | 451 | [LOGGING] 452 | 453 | # Logging modules to check that the string format arguments are in logging 454 | # function parameter format 455 | logging-modules=logging 456 | 457 | 458 | [IMPORTS] 459 | 460 | # Allow wildcard imports from modules that define __all__. 461 | allow-wildcard-with-all=no 462 | 463 | # Analyse import fallback blocks. This can be used to support both Python 2 and 464 | # 3 compatible code, which means that the block might have code that exists 465 | # only in one or another interpreter, leading to false positives when analysed. 466 | analyse-fallback-blocks=no 467 | 468 | # Deprecated modules which should not be used, separated by a comma 469 | deprecated-modules=regsub, 470 | TERMIOS, 471 | Bastion, 472 | rexec 473 | 474 | # Create a graph of external dependencies in the given file (report RP0402 must 475 | # not be disabled) 476 | ext-import-graph= 477 | 478 | # Create a graph of every (i.e. internal and external) dependencies in the 479 | # given file (report RP0402 must not be disabled) 480 | import-graph= 481 | 482 | # Create a graph of internal dependencies in the given file (report RP0402 must 483 | # not be disabled) 484 | int-import-graph= 485 | 486 | # Force import order to recognize a module as part of the standard 487 | # compatibility libraries. 488 | known-standard-library= 489 | 490 | # Force import order to recognize a module as part of a third party library. 491 | known-third-party=enchant 492 | 493 | 494 | [CLASSES] 495 | 496 | # List of method names used to declare (i.e. assign) instance attributes. 497 | defining-attr-methods=__init__, 498 | __new__, 499 | setUp 500 | 501 | # List of member names, which should be excluded from the protected access 502 | # warning. 503 | exclude-protected=_asdict, 504 | _fields, 505 | _replace, 506 | _source, 507 | _make 508 | 509 | # List of valid names for the first argument in a class method. 510 | valid-classmethod-first-arg=cls 511 | 512 | # List of valid names for the first argument in a metaclass class method. 513 | valid-metaclass-classmethod-first-arg=mcs 514 | 515 | 516 | [DESIGN] 517 | 518 | # Maximum number of arguments for function / method 519 | max-args=10 520 | 521 | # Maximum number of attributes for a class (see R0902). 522 | max-attributes=15 523 | 524 | # Maximum number of boolean expressions in a if statement 525 | max-bool-expr=5 526 | 527 | # Maximum number of branch for function / method body 528 | max-branches=12 529 | 530 | # Maximum number of locals for function / method body 531 | max-locals=20 532 | 533 | # Maximum number of parents for a class (see R0901). 534 | max-parents=7 535 | 536 | # Maximum number of public methods for a class (see R0904). 537 | max-public-methods=20 538 | 539 | # Maximum number of return / yield for function / method body 540 | max-returns=6 541 | 542 | # Maximum number of statements in function / method body 543 | max-statements=50 544 | 545 | # Minimum number of public methods for a class (see R0903). 546 | min-public-methods=0 547 | 548 | 549 | [EXCEPTIONS] 550 | 551 | # Exceptions that will emit a warning when being caught. Defaults to 552 | # "Exception" 553 | overgeneral-exceptions=Exception 554 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Development 2 | ----------- 3 | 4 | The following sections are intended for developers. 5 | 6 | Requirements 7 | ~~~~~~~~~~~~ 8 | 9 | Developers should use virtualenv to maintain multiple side-by-side 10 | environments to test with. Specifically, all contributions must be 11 | tested with both 2.7.6 and 3.4.3 to ensure the library is syntax 12 | compatible between the two versions. 13 | 14 | Dependencies 15 | ~~~~~~~~~~~~ 16 | 17 | Here was my basic setup on Mac OS X: 18 | 19 | | virtualenv --python=/usr/bin/python2.7 ~/.venv/pynuodb 20 | | source ~/.venv/pynuodb/bin/activate 21 | | pip install mock 22 | | pip install nose 23 | | pip install pytest 24 | | pip install coverage 25 | 26 | There are some commonly used libraries with this: 27 | 28 | | pip install sqlalchemy 29 | | pip install sqlalchemy-nuodb 30 | 31 | Or simply: 32 | 33 | | pip install -r requirements.txt 34 | 35 | Then once those are setup... 36 | 37 | Developer Testing 38 | ~~~~~~~~~~~~~~~~~ 39 | 40 | Prerequisite for testing is to unset LC_CTYPE: 41 | 42 | | unset LC_CTYPE 43 | 44 | My basic means of testing has been: 45 | 46 | | source ~/.venv/pynuodb/bin/activate 47 | | cd 48 | | py.test 49 | 50 | First and foremost, painful py.test less, it captures STDOUT and will not 51 | display your log messages. To disable this behavior and enable sane logging 52 | the following option is suggested: 53 | 54 | | --capture=no 55 | 56 | To stop on first failure you could augment that with the pdb option: 57 | 58 | | py.test --pdb 59 | 60 | To run a specific test you could do something like this: 61 | 62 | | py.test -k "SomeTest and test_something" 63 | 64 | Or any combination of the above, if you like. 65 | 66 | To gather coverage information you can run the following command: 67 | 68 | | py.test --cov=pynuodb --cov-report html --cov-report term-missing 69 | 70 | Developer Installation 71 | ~~~~~~~~~~~~~~~~~~~~~~ 72 | 73 | With pip installed, you can install this project via: 74 | 75 | | pip install -e . 76 | 77 | Release 78 | ------- 79 | 80 | Maintain the list of changes per release in CHANGES.rst. Also note the known defects. 81 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2023 Dassault Systemes SE 2 | All Rights Reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of Dassault Systemes SE nor the names of its 13 | contributors may be used to endorse or promote products derived from 14 | this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL DASSAULT SYSTEMES SE BE LIABLE FOR ANY DIRECT, 20 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 23 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 24 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 25 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2015-2025 Dassault Systemes SE. All Rights Reserved. 2 | # 3 | # This software is licensed under a BSD 3-Clause License. 4 | # See the LICENSE file provided with this software. 5 | 6 | PYTHON_VERSION ?= 3 7 | 8 | PYTHON ?= python$(PYTHON_VERSION) 9 | PIP ?= pip$(PYTHON_VERSION) 10 | 11 | VIRTUALENV ?= virtualenv 12 | MYPY ?= mypy 13 | PYLINT ?= pylint 14 | 15 | MKDIR ?= mkdir -p 16 | RMDIR ?= rm -rf 17 | 18 | TMPDIR ?= ./.testtemp 19 | VIRTDIR ?= ./.virttemp 20 | 21 | ARTIFACTDIR ?= artifacts 22 | RESULTSDIR ?= results 23 | 24 | PYTEST ?= pytest 25 | PYTEST_ARGS ?= 26 | PYTEST_LOG ?= --show-capture=stdout --log-file=$(ARTIFACTDIR)/testlog.out --log-file-level=INFO 27 | PYTEST_OPTS ?= --junitxml=$(RESULTSDIR)/result.xml 28 | PYTEST_COV ?= --cov=pynuodb --cov-report=html:$(ARTIFACTDIR) --cov-report=term-missing 29 | 30 | SUDO ?= sudo -n 31 | NUODB_HOME ?= /opt/nuodb 32 | 33 | _INSTALL_CMD = $(PIP) install '.[crypto]' 34 | _VERIFY_CMD = $(NUODB_HOME)/bin/nuocmd show domain 35 | _PYTEST_CMD = $(MKDIR) $(ARTIFACTDIR) $(RESULTSDIR) \ 36 | && TMPDIR='$(TMPDIR)' PATH="$(NUODB_HOME)/bin:$$PATH" \ 37 | $(PYTEST) $(PYTEST_LOG) $(PYTEST_OPTS) $(PYTEST_ARGS) 38 | 39 | all: 40 | $(MAKE) install 41 | $(MAKE) test 42 | 43 | install: 44 | $(_INSTALL_CMD) 45 | 46 | check: mypy pylint fulltest 47 | 48 | fulltest: 49 | $(_INSTALL_CMD) 50 | $(PIP) install -r test_requirements.txt 51 | $(_VERIFY_CMD) 52 | $(_PYTEST_CMD) 53 | 54 | test: 55 | $(_PYTEST_CMD) 56 | 57 | test-coverage: 58 | $(_PYTEST_CMD) $(PYTEST_COV) 59 | 60 | verify: 61 | $(_VERIFY_CMD) 62 | 63 | mypy: 64 | $(MYPY) --ignore-missing-imports pynuodb 65 | 66 | pylint: 67 | $(PYLINT) --rcfile=.pylint-rc pynuodb 68 | 69 | virtual-%: 70 | $(RMDIR) '$(VIRTDIR)' 71 | $(VIRTUALENV) -p $(PYTHON) '$(VIRTDIR)' 72 | . '$(VIRTDIR)/bin/activate' && $(MAKE) '$*' 73 | 74 | deploy: 75 | $(PYTHON) setup.py register 76 | $(PYTHON) setup.py sdist upload 77 | 78 | clean: 79 | $(RMDIR) build/ dist/ *.egg-info htmlcov/ 80 | 81 | doc: 82 | $(PIP) install epydoc 83 | epydoc --html --name PyNuoDB pynuodb/ 84 | cp epydoc.css html/ 85 | 86 | .PHONY: all install check fulltest test verify deploy clean doc 87 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | NuoDB - Python 3 | ============== 4 | 5 | .. image::https://circleci.com/gh/nuodb/nuodb-python.svg?style=svg 6 | :target: https://circleci.com/gh/nuodb/nuodb-python 7 | :alt: Test Results 8 | .. image:: https://gitlab.com/cadmin/nuodb-python/badges/master/pipeline.svg 9 | :target: https://gitlab.com/nuodb-mirror/nuodb-python/-/jobs 10 | :alt: Dependency Verification 11 | 12 | .. contents:: 13 | 14 | This package contains the community driven pure-Python NuoDB_ client library 15 | that provides a standard `PEP 249`_ SQL API. This is a community driven driver 16 | with limited support and testing from NuoDB. 17 | 18 | Requirements 19 | ------------ 20 | 21 | * Python -- one of the following: 22 | 23 | - CPython_ >= 2.7 24 | 25 | * NuoDB -- one of the following: 26 | 27 | - NuoDB_ >= 2.0.4 28 | 29 | If you don't have a NuoDB domain available you can create one using the Docker 30 | image on DockerHub. See `Quick Start Guides / Docker `_. 31 | 32 | Installation 33 | ------------ 34 | 35 | The current stable release is available on PyPI and can be installed with 36 | ``pip``:: 37 | 38 | $ pip install pynuodb 39 | 40 | Alternatively (e.g. if ``pip`` is not available), a tarball can be downloaded 41 | from GitHub and installed with Setuptools:: 42 | 43 | $ curl -L https://github.com/nuodb/nuodb-python/archive/master.tar.gz | tar xz 44 | $ cd nuodb-python* 45 | $ python setup.py install 46 | # The folder nuodb-python* can be safely removed now. 47 | 48 | Example 49 | ------- 50 | 51 | Here is an example using the `PEP 249`_ API that creates some tables, inserts 52 | some data, runs a query, and cleans up after itself: 53 | 54 | .. code:: python 55 | 56 | import pynuodb 57 | 58 | options = {"schema": "test"} 59 | connect_kw_args = {'database': "test", 'host': "localhost", 'user': "dba", 'password': "dba", 'options': options} 60 | 61 | connection = pynuodb.connect(**connect_kw_args) 62 | cursor = connection.cursor() 63 | try: 64 | stmt_drop = "DROP TABLE IF EXISTS names" 65 | cursor.execute(stmt_drop) 66 | 67 | stmt_create = """ 68 | CREATE TABLE names ( 69 | id BIGINT NOT NULL GENERATED ALWAYS AS IDENTITY PRIMARY KEY, 70 | name VARCHAR(30) DEFAULT '' NOT NULL, 71 | age INTEGER DEFAULT 0 72 | )""" 73 | cursor.execute(stmt_create) 74 | 75 | names = (('Greg', 17,), ('Marsha', 16,), ('Jan', 14,)) 76 | stmt_insert = "INSERT INTO names (name, age) VALUES (?, ?)" 77 | cursor.executemany(stmt_insert, names) 78 | 79 | connection.commit() 80 | 81 | age_limit = 15 82 | stmt_select = "SELECT id, name FROM names where age > ? ORDER BY id" 83 | cursor.execute(stmt_select, (age_limit,)) 84 | print("Results:") 85 | for row in cursor.fetchall(): 86 | print("%d | %s" % (row[0], row[1])) 87 | 88 | finally: 89 | cursor.execute(stmt_drop) 90 | cursor.close() 91 | connection.close() 92 | 93 | For further information on getting started with NuoDB, please refer to the Documentation_. 94 | 95 | Resources 96 | --------- 97 | 98 | DB-API 2.0: https://www.python.org/dev/peps/pep-0249/ 99 | 100 | NuoDB Documentation: https://doc.nuodb.com/nuodb/latest/introduction-to-nuodb/ 101 | 102 | License 103 | ------- 104 | 105 | PyNuoDB is licensed under a `BSD 3-Clause License `_. 106 | 107 | .. _Documentation: https://doc.nuodb.com/nuodb/latest/introduction-to-nuodb/ 108 | .. _NuoDB: https://www.nuodb.com/ 109 | .. _CPython: https://www.python.org/ 110 | .. _PEP 249: https://www.python.org/dev/peps/pep-0249/ 111 | -------------------------------------------------------------------------------- /epydoc.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | /* Epydoc CSS Stylesheet 4 | * 5 | * This stylesheet can be used to customize the appearance of epydoc's 6 | * HTML output. 7 | * 8 | */ 9 | 10 | /* Default Colors & Styles 11 | * - Set the default foreground & background color with 'body'; and 12 | * link colors with 'a:link' and 'a:visited'. 13 | * - Use bold for decision list terms. 14 | * - The heading styles defined here are used for headings *within* 15 | * docstring descriptions. All headings used by epydoc itself use 16 | * either class='epydoc' or class='toc' (CSS styles for both 17 | * defined below). 18 | */ 19 | body { background: #ffffff; color: #000000; } 20 | p { margin-top: 0.5em; margin-bottom: 0.5em; } 21 | a:link { color: #000000; } 22 | a:visited { color: #000000; } 23 | dt { font-weight: bold; } 24 | h1 { font-size: +140%; font-style: italic; 25 | font-weight: bold; } 26 | h2 { font-size: +125%; font-style: italic; 27 | font-weight: bold; } 28 | h3 { font-size: +110%; font-style: italic; 29 | font-weight: normal; } 30 | code { font-size: 100%; } 31 | /* N.B.: class, not pseudoclass */ 32 | a.link { font-family: monospace; } 33 | 34 | /* Page Header & Footer 35 | * - The standard page header consists of a navigation bar (with 36 | * pointers to standard pages such as 'home' and 'trees'); a 37 | * breadcrumbs list, which can be used to navigate to containing 38 | * classes or modules; options links, to show/hide private 39 | * variables and to show/hide frames; and a page title (using 40 | *

). The page title may be followed by a link to the 41 | * corresponding source code (using 'span.codelink'). 42 | * - The footer consists of a navigation bar, a timestamp, and a 43 | * pointer to epydoc's homepage. 44 | */ 45 | h1.epydoc { margin: 0; font-size: +140%; font-weight: bold; } 46 | h2.epydoc { font-size: +130%; font-weight: bold; } 47 | h3.epydoc { font-size: +115%; font-weight: bold; 48 | margin-top: 0.2em; } 49 | td h3.epydoc { font-size: +115%; font-weight: bold; 50 | margin-bottom: 0; } 51 | table.navbar { background: #c1cd23; color: #000000; 52 | border: 2px groove #c0d0d0; } 53 | table.navbar table { color: #000000; } 54 | th.navbar-select { background: #FFFFFF; 55 | color: #000000; } 56 | table.navbar a { text-decoration: none; } 57 | table.navbar a:link { color: #000000; } 58 | table.navbar a:visited { color: #000000; } 59 | span.breadcrumbs { font-size: 85%; font-weight: bold; } 60 | span.options { font-size: 70%; } 61 | span.codelink { font-size: 85%; } 62 | td.footer { font-size: 85%; } 63 | 64 | /* Table Headers 65 | * - Each summary table and details section begins with a 'header' 66 | * row. This row contains a section title (marked by 67 | * 'span.table-header') as well as a show/hide private link 68 | * (marked by 'span.options', defined above). 69 | * - Summary tables that contain user-defined groups mark those 70 | * groups using 'group header' rows. 71 | */ 72 | td.table-header { background: #c1cd23; color: #000000; 73 | border: 1px solid #608090; } 74 | td.table-header table { color: #FFFFFF; } 75 | td.table-header table a:link { color: #000000; } 76 | td.table-header table a:visited { color: #000000; } 77 | span.table-header { font-size: 120%; font-weight: bold; } 78 | th.group-header { background: #c0e0f8; color: #000000; 79 | text-align: left; font-style: italic; 80 | font-size: 115%; 81 | border: 1px solid #608090; } 82 | 83 | /* Summary Tables (functions, variables, etc) 84 | * - Each object is described by a single row of the table with 85 | * two cells. The left cell gives the object's type, and is 86 | * marked with 'code.summary-type'. The right cell gives the 87 | * object's name and a summary description. 88 | * - CSS styles for the table's header and group headers are 89 | * defined above, under 'Table Headers' 90 | */ 91 | table.summary { border-collapse: collapse; 92 | background: #E6EBA7; color: #000000; 93 | border: 1px solid #608090; 94 | margin-bottom: 0.5em; } 95 | td.summary { border: 1px solid #5F584E; } 96 | code.summary-type { font-size: 85%; } 97 | table.summary a:link { color: #000000; } 98 | table.summary a:visited { color: #000000; } 99 | 100 | 101 | /* Details Tables (functions, variables, etc) 102 | * - Each object is described in its own div. 103 | * - A single-row summary table w/ table-header is used as 104 | * a header for each details section (CSS style for table-header 105 | * is defined above, under 'Table Headers'). 106 | */ 107 | table.details { border-collapse: collapse; 108 | background: #E6EBA7; color: #000000; 109 | border: 1px solid #608090; 110 | margin: .2em 0 0 0; } 111 | table.details table { color: #000000; } 112 | table.details a:link { color: #000000; } 113 | table.details a:visited { color: #000000; } 114 | 115 | /* Fields */ 116 | dl.fields { margin-left: 2em; margin-top: 1em; 117 | margin-bottom: 1em; } 118 | dl.fields dd ul { margin-left: 0em; padding-left: 0em; } 119 | dl.fields dd ul li ul { margin-left: 2em; padding-left: 0em; } 120 | div.fields { margin-left: 2em; } 121 | div.fields p { margin-bottom: 0.5em; } 122 | 123 | /* Index tables (identifier index, term index, etc) 124 | * - link-index is used for indices containing lists of links 125 | * (namely, the identifier index & term index). 126 | * - index-where is used in link indices for the text indicating 127 | * the container/source for each link. 128 | * - metadata-index is used for indices containing metadata 129 | * extracted from fields (namely, the bug index & todo index). 130 | */ 131 | table.link-index { border-collapse: collapse; 132 | background: #E6EBA7; color: #000000; 133 | border: 1px solid #608090; } 134 | td.link-index { border-width: 0px; } 135 | table.link-index a:link { color: #000000; } 136 | table.link-index a:visited { color: #000000; } 137 | span.index-where { font-size: 70%; } 138 | table.metadata-index { border-collapse: collapse; 139 | background: #e8f0f8; color: #000000; 140 | border: 1px solid #608090; 141 | margin: .2em 0 0 0; } 142 | td.metadata-index { border-width: 1px; border-style: solid; } 143 | table.metadata-index a:link { color: #0000ff; } 144 | table.metadata-index a:visited { color: #204080; } 145 | 146 | /* Function signatures 147 | * - sig* is used for the signature in the details section. 148 | * - .summary-sig* is used for the signature in the summary 149 | * table, and when listing property accessor functions. 150 | * */ 151 | .sig-name { color: #006080; } 152 | .sig-arg { color: #008060; } 153 | .sig-default { color: #602000; } 154 | .summary-sig { font-family: monospace; } 155 | .summary-sig-name { color: #006080; font-weight: bold; } 156 | table.summary a.summary-sig-name:link 157 | { color: #000000; font-weight: bold; } 158 | table.summary a.summary-sig-name:visited 159 | { color: #006080; font-weight: bold; } 160 | .summary-sig-arg { color: #006040; } 161 | .summary-sig-default { color: #501800; } 162 | 163 | /* Subclass list 164 | */ 165 | ul.subclass-list { display: inline; } 166 | ul.subclass-list li { display: inline; } 167 | 168 | /* To render variables, classes etc. like functions */ 169 | table.summary .summary-name { color: #006080; font-weight: bold; 170 | font-family: monospace; } 171 | table.summary 172 | a.summary-name:link { color: #006080; font-weight: bold; 173 | font-family: monospace; } 174 | table.summary 175 | a.summary-name:visited { color: #006080; font-weight: bold; 176 | font-family: monospace; } 177 | 178 | /* Variable values 179 | * - In the 'variable details' sections, each varaible's value is 180 | * listed in a 'pre.variable' box. The width of this box is 181 | * restricted to 80 chars; if the value's repr is longer than 182 | * this it will be wrapped, using a backslash marked with 183 | * class 'variable-linewrap'. If the value's repr is longer 184 | * than 3 lines, the rest will be ellided; and an ellipsis 185 | * marker ('...' marked with 'variable-ellipsis') will be used. 186 | * - If the value is a string, its quote marks will be marked 187 | * with 'variable-quote'. 188 | * - If the variable is a regexp, it is syntax-highlighted using 189 | * the re* CSS classes. 190 | */ 191 | pre.variable { padding: .5em; margin: 0; 192 | background: #887e6f; color: #000000; 193 | border: 1px solid #708890; } 194 | .variable-linewrap { color: #604000; font-weight: bold; } 195 | .variable-ellipsis { color: #604000; font-weight: bold; } 196 | .variable-quote { color: #604000; font-weight: bold; } 197 | .variable-group { color: #008000; font-weight: bold; } 198 | .variable-op { color: #604000; font-weight: bold; } 199 | .variable-string { color: #006030; } 200 | .variable-unknown { color: #a00000; font-weight: bold; } 201 | .re { color: #000000; } 202 | .re-char { color: #006030; } 203 | .re-op { color: #600000; } 204 | .re-group { color: #003060; } 205 | .re-ref { color: #404040; } 206 | 207 | /* Base tree 208 | * - Used by class pages to display the base class hierarchy. 209 | */ 210 | pre.base-tree { font-size: 80%; margin: 0; } 211 | 212 | /* Frames-based table of contents headers 213 | * - Consists of two frames: one for selecting modules; and 214 | * the other listing the contents of the selected module. 215 | * - h1.toc is used for each frame's heading 216 | * - h2.toc is used for subheadings within each frame. 217 | */ 218 | h1.toc { text-align: center; font-size: 105%; 219 | margin: 0; font-weight: bold; 220 | padding: 0; } 221 | h2.toc { font-size: 100%; font-weight: bold; 222 | margin: 0.5em 0 0 -0.3em; } 223 | 224 | /* Syntax Highlighting for Source Code 225 | * - doctest examples are displayed in a 'pre.py-doctest' block. 226 | * If the example is in a details table entry, then it will use 227 | * the colors specified by the 'table pre.py-doctest' line. 228 | * - Source code listings are displayed in a 'pre.py-src' block. 229 | * Each line is marked with 'span.py-line' (used to draw a line 230 | * down the left margin, separating the code from the line 231 | * numbers). Line numbers are displayed with 'span.py-lineno'. 232 | * The expand/collapse block toggle button is displayed with 233 | * 'a.py-toggle' (Note: the CSS style for 'a.py-toggle' should not 234 | * modify the font size of the text.) 235 | * - If a source code page is opened with an anchor, then the 236 | * corresponding code block will be highlighted. The code 237 | * block's header is highlighted with 'py-highlight-hdr'; and 238 | * the code block's body is highlighted with 'py-highlight'. 239 | * - The remaining py-* classes are used to perform syntax 240 | * highlighting (py-string for string literals, py-name for names, 241 | * etc.) 242 | */ 243 | pre.py-doctest { padding: .5em; margin: 1em; 244 | background: #e8f0f8; color: #000000; 245 | border: 1px solid #708890; } 246 | table pre.py-doctest { background: #dce4ec; 247 | color: #000000; } 248 | pre.py-src { border: 2px solid #000000; 249 | background: #f0f0f0; color: #000000; } 250 | .py-line { border-left: 2px solid #000000; 251 | margin-left: .2em; padding-left: .4em; } 252 | .py-lineno { font-style: italic; font-size: 90%; 253 | padding-left: .5em; } 254 | a.py-toggle { text-decoration: none; } 255 | div.py-highlight-hdr { border-top: 2px solid #000000; 256 | border-bottom: 2px solid #000000; 257 | background: #d8e8e8; } 258 | div.py-highlight { border-bottom: 2px solid #000000; 259 | background: #d0e0e0; } 260 | .py-prompt { color: #005050; font-weight: bold;} 261 | .py-more { color: #005050; font-weight: bold;} 262 | .py-string { color: #006030; } 263 | .py-comment { color: #003060; } 264 | .py-keyword { color: #600000; } 265 | .py-output { color: #404040; } 266 | .py-name { color: #000050; } 267 | .py-name:link { color: #000000 !important; } 268 | .py-name:visited { color: #000000 !important; } 269 | .py-number { color: #005000; } 270 | .py-defname { color: #000060; font-weight: bold; } 271 | .py-def-name { color: #000060; font-weight: bold; } 272 | .py-base-class { color: #000060; } 273 | .py-param { color: #000060; } 274 | .py-docstring { color: #006030; } 275 | .py-decorator { color: #804020; } 276 | /* Use this if you don't want links to names underlined: */ 277 | /*a.py-name { text-decoration: none; }*/ 278 | 279 | /* Graphs & Diagrams 280 | * - These CSS styles are used for graphs & diagrams generated using 281 | * Graphviz dot. 'img.graph-without-title' is used for bare 282 | * diagrams (to remove the border created by making the image 283 | * clickable). 284 | */ 285 | img.graph-without-title { border: none; } 286 | img.graph-with-title { border: 1px solid #000000; } 287 | span.graph-title { font-weight: bold; } 288 | span.graph-caption { } 289 | 290 | /* General-purpose classes 291 | * - 'p.indent-wrapped-lines' defines a paragraph whose first line 292 | * is not indented, but whose subsequent lines are. 293 | * - The 'nomargin-top' class is used to remove the top margin (e.g. 294 | * from lists). The 'nomargin' class is used to remove both the 295 | * top and bottom margin (but not the left or right margin -- 296 | * for lists, that would cause the bullets to disappear.) 297 | */ 298 | p.indent-wrapped-lines { padding: 0 0 0 7em; text-indent: -7em; 299 | margin: 0; } 300 | .nomargin-top { margin-top: 0; } 301 | .nomargin { margin-top: 0; margin-bottom: 0; } 302 | 303 | /* HTML Log */ 304 | div.log-block { padding: 0; margin: .5em 0 .5em 0; 305 | background: #e8f0f8; color: #000000; 306 | border: 1px solid #000000; } 307 | div.log-error { padding: .1em .3em .1em .3em; margin: 4px; 308 | background: #ffb0b0; color: #000000; 309 | border: 1px solid #000000; } 310 | div.log-warning { padding: .1em .3em .1em .3em; margin: 4px; 311 | background: #ffffb0; color: #000000; 312 | border: 1px solid #000000; } 313 | div.log-info { padding: .1em .3em .1em .3em; margin: 4px; 314 | background: #b0ffb0; color: #000000; 315 | border: 1px solid #000000; } 316 | h2.log-hdr { background: #c1cd23; color: #000000; 317 | margin: 0; padding: 0em 0.5em 0em 0.5em; 318 | border-bottom: 1px solid #000000; font-size: 110%; } 319 | p.log { font-weight: bold; margin: .5em 0 .5em 0; } 320 | tr.opt-changed { color: #000000; font-weight: bold; } 321 | tr.opt-default { color: #606060; } 322 | pre.log { margin: 0; padding: 0; padding-left: 1em; } 323 | -------------------------------------------------------------------------------- /pynuodb/__init__.py: -------------------------------------------------------------------------------- 1 | """An implementation of Python PEP 249 for NuoDB. 2 | 3 | (C) Copyright 2013-2023 Dassault Systemes SE. All Rights Reserved. 4 | 5 | This software is licensed under a BSD 3-Clause License. 6 | See the LICENSE file provided with this software. 7 | """ 8 | 9 | __version__ = '3.0.0' 10 | 11 | from .connection import * # pylint: disable=wildcard-import 12 | from .datatype import * # pylint: disable=wildcard-import 13 | from .exception import * # pylint: disable=wildcard-import, redefined-builtin 14 | -------------------------------------------------------------------------------- /pynuodb/connection.py: -------------------------------------------------------------------------------- 1 | """A module for connecting to a NuoDB database. 2 | 3 | (C) Copyright 2013-2025 Dassault Systemes SE. All Rights Reserved. 4 | 5 | This software is licensed under a BSD 3-Clause License. 6 | See the LICENSE file provided with this software. 7 | 8 | Exported Classes: 9 | Connection -- Class for establishing connection with host. 10 | 11 | Exported Functions: 12 | connect -- Creates a connection object. 13 | """ 14 | 15 | __all__ = ['apilevel', 'threadsafety', 'paramstyle', 'connect', 16 | 'reset', 'Connection'] 17 | 18 | import os 19 | import copy 20 | import time 21 | import xml.etree.ElementTree as ElementTree 22 | 23 | try: 24 | from typing import Any, Dict, Mapping, Optional, Tuple # pylint: disable=unused-import 25 | except ImportError: 26 | pass 27 | 28 | from . import __version__ 29 | from .exception import Error, InterfaceError 30 | from .session import SessionException 31 | 32 | from . import cursor 33 | from . import session 34 | from . import encodedsession 35 | 36 | apilevel = "2.0" 37 | threadsafety = 1 38 | paramstyle = "qmark" 39 | 40 | 41 | def connect(database=None, # type: Optional[str] 42 | host=None, # type: Optional[str] 43 | user=None, # type: Optional[str] 44 | password=None, # type: Optional[str] 45 | options=None, # type: Optional[Mapping[str, str]] 46 | **kwargs 47 | ): 48 | # type: (...) -> Connection 49 | """Return a new NuoDB SQL Connection object. 50 | 51 | :param database: Name of the database. 52 | :param host: Hostname (and port if non-default) of the AP to connect to. 53 | :param user: Username to connect with. 54 | :param password: Password to connect with. 55 | :param options: Connection options. 56 | :returns: A new Connection object. 57 | """ 58 | return Connection(database=database, host=host, 59 | user=user, password=password, 60 | options=options, **kwargs) 61 | 62 | 63 | def reset(): 64 | # type: () -> None 65 | """Reset the module to its initial state. 66 | 67 | Forget any global state maintained by the module. 68 | NOTE: this does not impact existing connections or cursors. 69 | It only impacts new connections. 70 | """ 71 | encodedsession.EncodedSession.reset() 72 | 73 | 74 | class Connection(object): 75 | """An established SQL connection with a NuoDB database. 76 | 77 | Public Functions: 78 | testConnection -- Tests to ensure the connection was properly established. 79 | close -- Closes the connection with the host. 80 | commit -- Sends a message to the host to commit transaction. 81 | rollback -- Sends a message to the host to rollback uncommitted changes. 82 | cursor -- Return a new Cursor object using the connection. 83 | setautocommit -- Change the auto-commit mode of the connection. 84 | 85 | Private Functions: 86 | __init__ -- Constructor for the Connection class. 87 | _check_closed -- Checks if the connection to the host is closed. 88 | 89 | Special Function: 90 | autocommit (getter) -- Gets the value of auto-commit from the database. 91 | autocommit (setter) -- Sets the value of auto-commit on the database. 92 | 93 | Deprecated: These names were used in older versions of the driver but they 94 | are not part of PEP 249. 95 | auto_commit (getter) -- Gets the value of auto-commit from the database. 96 | auto_commit (setter) -- Sets the value of auto-commit on the database. 97 | """ 98 | 99 | # PEP 249 recommends that all exceptions be exposed as attributes in the 100 | # Connection object. 101 | from .exception import Warning, Error, InterfaceError, DatabaseError 102 | from .exception import OperationalError, IntegrityError, InternalError 103 | from .exception import ProgrammingError, NotSupportedError 104 | 105 | _trans_id = None # type: Optional[int] 106 | __session = None # type: encodedsession.EncodedSession 107 | 108 | __config = None # type: Dict[str, Any] 109 | 110 | def __init__(self, database=None, # type: Optional[str] 111 | host=None, # type: Optional[str] 112 | user=None, # type: Optional[str] 113 | password=None, # type: Optional[str] 114 | options=None, # type: Optional[Mapping[str, str]] 115 | **kwargs 116 | ): 117 | # type: (...) -> None 118 | """Construct a Connection object. 119 | 120 | :param database: Name of the database to connect to 121 | :param host: Host (and port if needed) of the AP to connect to. 122 | :param username: Username to connect with. 123 | :param password: Password to connect with. 124 | :param options: Connection options. 125 | :param kwargs: Extra arguments to pass to EncodedSession. 126 | """ 127 | if database is None: 128 | raise InterfaceError("No database provided.") 129 | if user is None: 130 | raise InterfaceError("No user provided.") 131 | if password is None: 132 | raise InterfaceError("No password provided.") 133 | 134 | self.__config = {'driver_version': __version__, 135 | 'db_name': database, 136 | 'user': user, 137 | 'options': copy.deepcopy(options)} 138 | 139 | # Split the options into connection parameters and session options 140 | params, opts = session.Session.session_options(options) 141 | 142 | params['Database'] = database 143 | 144 | if host is None: 145 | host = 'localhost' 146 | 147 | port = None 148 | direct = opts.get('direct', 'false') 149 | if direct.lower() == 'true': 150 | self.__config['ap_host'] = None 151 | # In direct mode the port is part of the host string already 152 | self.__config['engine_host'] = host 153 | else: 154 | self.__config['ap_host'] = host 155 | # Pass all the connection parameters to the AP just in case. 156 | (host, port) = self._getTE(host, params, opts) 157 | self.__config['engine_host'] = '%s:%d' % (host, port) 158 | 159 | # Connect to the NuoDB TE. It needs all the options. 160 | self.__session = encodedsession.EncodedSession( 161 | host, port=port, options=options, **kwargs) 162 | self.__session.doConnect(params) 163 | 164 | params.update({'user': user, 165 | 'timezone': time.strftime('%Z'), 166 | 'clientProcessId': str(os.getpid())}) 167 | 168 | self.__session.open_database(database, password, params) 169 | 170 | self.__config['client_protocol_id'] = self.__session.protocol_id 171 | self.__config['connection_id'] = self.__session.connection_id 172 | self.__config['db_uuid'] = self.__session.db_uuid 173 | self.__config['db_protocol_id'] = self.__session.db_protocol_id 174 | self.__config['engine_id'] = self.__session.engine_id 175 | 176 | if self.__session.tls_encrypted: 177 | self.__config['tls_enabled'] = True 178 | self.__config['cipher'] = None 179 | else: 180 | self.__config['tls_enabled'] = False 181 | self.__config['cipher'] = self.__session.cipher_name 182 | 183 | # Set auto commit to false by default per PEP 249 184 | if 'autocommit' in kwargs: 185 | self.setautocommit(kwargs['autocommit']) 186 | else: 187 | self.setautocommit(False) 188 | 189 | @staticmethod 190 | def _getTE(admin, attributes, options): 191 | # type: (str, Mapping[str, str], Mapping[str, str]) -> Tuple[str, int] 192 | """Connect to the AP and ask it to direct us a TE for this database.""" 193 | s = session.Session(admin, service="SQL2", options=options) 194 | try: 195 | s.doConnect(attributes=attributes) 196 | connectDetail = s.recv() 197 | finally: 198 | s.close() 199 | 200 | if connectDetail is None: 201 | # Since we don't pass a timeout to recv, it must return a value 202 | raise RuntimeError("Session.rev() returned None without timeout!") 203 | 204 | connString = connectDetail.decode() 205 | session.checkForError(connString) 206 | 207 | root = ElementTree.fromstring(connString) 208 | if root.tag != "Cloud": 209 | raise SessionException("Unexpected AP response type: " + root.tag) 210 | address = root.get('Address') 211 | if address is None: 212 | raise SessionException("Invalid AP response: missing address") 213 | port = root.get('Port') 214 | if port is None: 215 | raise SessionException("Invalid AP response: missing port") 216 | return (address, int(port)) 217 | 218 | def testConnection(self): 219 | # type: () -> None 220 | """Ensure the connection was properly established. 221 | 222 | :raises ProgrammingError: If the connection is not established. 223 | """ 224 | self.__session.test_connection() 225 | 226 | @property 227 | def autocommit(self): 228 | # type: () -> bool 229 | """Return the value of autocommit for the connection. 230 | 231 | :returns: True if autocommit is enabled. 232 | """ 233 | self._check_closed() 234 | return self.__session.get_autocommit() == 1 235 | 236 | @autocommit.setter 237 | def autocommit(self, value): 238 | # type: (bool) -> None 239 | """Set the value of autocommit for the connection. 240 | 241 | :param bool value: True to enable autocommit, False to disable. 242 | """ 243 | self.setautocommit(value) 244 | 245 | @property 246 | def auto_commit(self): 247 | # type: () -> int 248 | """Return the value of autocommit for the connection. 249 | 250 | DEPRECATED. 251 | :returns: 0 if autocommit is not enabled, 1 if it is enabled. 252 | """ 253 | return self.autocommit 254 | 255 | @auto_commit.setter 256 | def auto_commit(self, value): 257 | # type: (int) -> None 258 | """Set the value of auto_commit for the connection. 259 | 260 | DEPRECATED. 261 | :param value: 1 to enable autocommit, 0 to disable. 262 | """ 263 | self.setautocommit(value != 0) 264 | 265 | def connection_config(self): 266 | # type: () -> Dict[str, Any] 267 | """Returns a copy of the connection configuration. 268 | 269 | Configuration: 270 | ap_host :str: Address of the AP host or None for direct 271 | cipher :str: Name of the cipher if using SRP, else None 272 | connected :bool: True if the connection is active 273 | connection_id :int: ID of the connection 274 | db_name :str: name of the connected database 275 | db_protocol_id :int: Server protocol ID of the connected database 276 | db_uuid :uuid: UUID for the connected database 277 | driver_version :str: Version of this driver 278 | engine_host :str: Address of the TE we're connected to 279 | engine_id :int: ID for the TE we're connected to 280 | options :dict: Dictionary of connection options 281 | protocol_id :int: Negotiated client protocol ID 282 | tls_enabled :bool: True if we're connected using TLS 283 | user :str: name of the connected user 284 | 285 | :returns: Copy of the connection config names and values. 286 | Modifying these values has no effect on the connection. 287 | """ 288 | config = copy.deepcopy(self.__config) 289 | config['connected'] = not self.__session.closed 290 | return config 291 | 292 | def setautocommit(self, value): 293 | # type: (bool) -> None 294 | """Change the auto-commit status of the connection.""" 295 | self._check_closed() 296 | self.__session.set_autocommit(1 if value else 0) 297 | 298 | def close(self): 299 | # type: () -> None 300 | """Close this connection to the database.""" 301 | self._check_closed() 302 | self.__session.send_close() 303 | self.__session.closed = True 304 | 305 | def _check_closed(self): 306 | # type: () -> None 307 | """Check if the connection is available. 308 | 309 | :raises Error: If the connection to the host is closed. 310 | """ 311 | if self.__session.closed: 312 | raise Error("connection is closed") 313 | 314 | def commit(self): 315 | # type: () -> None 316 | """Commit the current transaction.""" 317 | self._check_closed() 318 | self._trans_id = self.__session.send_commit() 319 | 320 | def rollback(self): 321 | # type: () -> None 322 | """Rollback any uncommitted changes.""" 323 | self._check_closed() 324 | self.__session.send_rollback() 325 | 326 | def cursor(self, prepared_statement_cache_size=50): 327 | # type: (int) -> cursor.Cursor 328 | """Return a new Cursor object using the connection. 329 | 330 | :param cache_size: Size of the prepared statement cache. 331 | """ 332 | self._check_closed() 333 | return cursor.Cursor(self.__session, prepared_statement_cache_size) 334 | -------------------------------------------------------------------------------- /pynuodb/crypt.py: -------------------------------------------------------------------------------- 1 | """Manage encryption. 2 | 3 | (C) Copyright 2013-2025 Dassault Systemes SE. All Rights Reserved. 4 | 5 | This software is licensed under a BSD 3-Clause License. 6 | See the LICENSE file provided with this software. 7 | """ 8 | 9 | # This module provides the basic cryptographic routines (SRP and RC4) used to 10 | # establish authenticated, confidential sessions with engines. Note that no 11 | # protocols are implemented here, just the ability to calculate and use 12 | # session keys. Most users should never need the routines here, since they are 13 | # encapsulated in other classes, but they are available. 14 | # 15 | # For a client, the typical pattern is: 16 | # 17 | # cp = ClientPassword() 18 | # clientPub = cp.genClientKey() 19 | # 20 | # [ send 'clientPub' and get 'salt' and 'serverPub' from the server] 21 | # 22 | # sessionKey = cp.computeSessionKey('user', 'password', salt, serverKey) 23 | # cipherIn = RC4Cipher(False, sessionKey) 24 | # cipherOut = RC4Cipher(True, sessionKey) 25 | 26 | # Encodings: We want to convert to/from a full 8-bit character value. We 27 | # can't use utf-8 here since it reserves some of the 255 values to introduce 28 | # multi-byte values. And we can't use ASCII because it's a 7-bit code. The 29 | # 'latin-1' encoding allows all values 0-255 to be mapped to the "standard" 30 | # byte values. 31 | 32 | import hashlib 33 | import random 34 | import binascii 35 | import sys 36 | 37 | try: 38 | from typing import Optional # pylint: disable=unused-import 39 | except ImportError: 40 | pass 41 | 42 | try: 43 | import warnings 44 | with warnings.catch_warnings(): 45 | warnings.simplefilter('ignore') 46 | from cryptography.hazmat.backends import default_backend 47 | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes 48 | AESImported = True 49 | try: 50 | # Newer cryptography versions stash ARC4 here 51 | from cryptography.hazmat.decrepit.ciphers.algorithms import ARC4 52 | except ImportError: 53 | # In older cryptography it's still with the regular algorithms 54 | ARC4 = algorithms.ARC4 55 | arc4Imported = True 56 | except ImportError: 57 | arc4Imported = False 58 | AESImported = False 59 | 60 | isP2 = sys.version[0] == '2' 61 | 62 | 63 | def get_ciphers(): 64 | # type: () -> str 65 | """Return the list of ciphers supported by this client.""" 66 | return ("AES-256-CTR,AES-128-CTR," if AESImported else '') + "RC4" 67 | 68 | 69 | # We use a bytearray for our sending buffer because we need to construct it. 70 | # If we were using only Python3, then our received data could be stored in a 71 | # bytes object which might be slightly more efficient. But in Python2 a bytes 72 | # object is the same as a string so it can't be portable: if use bytes we need 73 | # different code to extract it on P2 vs. P3. 74 | # 75 | # Instead, we'll use a bytearray for the received data as well as the sending 76 | # data for as long as we need to support Python 2. 77 | 78 | if isP2: 79 | def bytesToArray(data): 80 | # type: (bytes) -> bytearray 81 | """Convert bytes to a bytearray. 82 | 83 | On Python 2 bytes is a string so we have to ord each character. 84 | """ 85 | return bytearray([ord(c) for c in data]) # type: ignore 86 | 87 | def arrayToStr(data): 88 | # type: (bytearray) -> str 89 | """Convert a bytearray to a str. 90 | 91 | On Python 2 we can just use the str() constructor. If we use decode 92 | we get back a unicode string not a string.. 93 | """ 94 | return str(data) 95 | 96 | def hexstrToBytes(hexstr): 97 | # type: (Optional[str]) -> Optional[bytes] 98 | """Convert a hex string to bytes.""" 99 | return binascii.unhexlify(hexstr) if hexstr is not None else None 100 | else: 101 | def bytesToArray(data): 102 | # type: (bytes) -> bytearray 103 | """Convert bytes to a bytearray. 104 | 105 | On Python 3 bytes is a binary string so we can just convert it. 106 | """ 107 | return bytearray(data) 108 | 109 | def arrayToStr(data): 110 | # type: (bytearray) -> str 111 | """Convert a bytearray to a str. 112 | 113 | On Python 3 we must decode: assume UTF-8 always. 114 | """ 115 | return data.decode('utf-8') 116 | 117 | def hexstrToBytes(hexstr): 118 | # type: (Optional[str]) -> Optional[bytes] 119 | """Convert a hex string to bytes.""" 120 | return bytes.fromhex(hexstr) if hexstr is not None else None # pylint: disable=no-member 121 | 122 | 123 | def toHex(bigInt): 124 | # type: (int) -> str 125 | """Convert an integer into a hex string.""" 126 | if isP2: 127 | hexStr = (hex(bigInt)[2:])[:-1] 128 | else: 129 | # Python 3 will no longer insert an L for type formatting 130 | hexStr = hex(bigInt)[2:] 131 | # Some platforms assume hex strings are even length: add padding if needed 132 | if len(hexStr) % 2 == 1: 133 | hexStr = '0' + hexStr 134 | return hexStr.upper() 135 | 136 | 137 | def fromHex(hexStr): 138 | # type: (str) -> int 139 | """Convert a hex string into an integer.""" 140 | return int(hexStr, 16) 141 | 142 | 143 | def toSignedByteString(value): 144 | # type: (int) -> bytearray 145 | """Convert an integer into bytes.""" 146 | result = bytearray() 147 | if value == 0 or value == -1: 148 | result.append(value & 0xFF) 149 | else: 150 | while value != 0 and value != -1: 151 | result.append(value & 0xFF) 152 | value >>= 8 153 | # Zero pad if positive 154 | if value == 0 and (result[-1] & 0x80) == 0x80: 155 | result.append(0x00) 156 | elif value == -1 and (result[-1] & 0x80) == 0x00: 157 | result.append(0xFF) 158 | result.reverse() 159 | return result 160 | 161 | 162 | def fromSignedByteString(data): 163 | # type: (bytearray) -> int 164 | """Convert bytes into a signed integer.""" 165 | if data: 166 | is_neg = (data[0] & 0x80) >> 7 167 | else: 168 | is_neg = 0 169 | result = 0 170 | shiftCount = 0 171 | for b in reversed(data): 172 | result = result | (((b & 0xFF) ^ (is_neg * 0xFF)) << shiftCount) 173 | shiftCount += 8 174 | 175 | return ((-1)**is_neg) * (result + is_neg) 176 | 177 | 178 | def toByteString(bigInt): 179 | # type: (int) -> bytearray 180 | """Convert an integer into bytes.""" 181 | result = bytearray() 182 | if bigInt == -1 or bigInt == 0: 183 | result.append(bigInt & 0xFF) 184 | else: 185 | while bigInt != 0 and bigInt != -1: 186 | result.append(bigInt & 0xFF) 187 | bigInt >>= 8 188 | result.reverse() 189 | return result 190 | 191 | 192 | def fromByteString(data): 193 | # type: (bytearray) -> int 194 | """Convert bytes into an integer.""" 195 | result = 0 196 | shiftCount = 0 197 | for b in reversed(data): 198 | result = result | ((b & 0xff) << shiftCount) 199 | shiftCount += 8 200 | return result 201 | 202 | 203 | class RemoteGroup(object): 204 | """A remote group.""" 205 | 206 | defaultPrime = ("EEAF0AB9ADB38DD69C33F80AFA8FC5E86072618775FF3C0B9EA2314C" 207 | "9C256576D674DF7496EA81D3383B4813D692C6E0E0D5D8E250B98BE4" 208 | "8E495C1D6089DAD15DC7D7B46154D6B6CE8EF4AD69B15D4982559B29" 209 | "7BCF1885C529F566660E57EC68EDBC3C05726CC02FD4CBF4976EAA9A" 210 | "FD5138FE8376435B9FC61D2FC0EB06E3") 211 | 212 | defaultGenerator = "2" 213 | 214 | def __init__(self, prime=defaultPrime, generator=defaultGenerator): 215 | # type: (str, str) -> None 216 | """Create a RemoteGroup. 217 | 218 | :param prime: Prime string. 219 | :param generator: Generator. 220 | """ 221 | self.__prime = fromHex(prime) 222 | self.__generator = fromHex(generator) 223 | 224 | primeBytes = toByteString(self.__prime) 225 | generatorBytes = toByteString(self.__generator) 226 | paddingLength = len(primeBytes) - len(generatorBytes) 227 | paddingBuffer = bytearray(paddingLength) 228 | 229 | md = hashlib.sha1() 230 | md.update(primeBytes) 231 | if paddingLength > 0: 232 | md.update(paddingBuffer) 233 | md.update(generatorBytes) 234 | self.__k = fromByteString(bytesToArray(md.digest())) 235 | 236 | def getPrime(self): 237 | # type: () -> int 238 | """Return the prime.""" 239 | return self.__prime 240 | 241 | def getGenerator(self): 242 | # type: () -> int 243 | """Return the generator.""" 244 | return self.__generator 245 | 246 | def getK(self): 247 | # type: () -> int 248 | """Return K.""" 249 | return self.__k 250 | 251 | 252 | class RemotePassword(object): 253 | """Manage the remote password.""" 254 | 255 | def __init__(self): 256 | # type: () -> None 257 | self.__group = RemoteGroup() 258 | 259 | @staticmethod 260 | def _getUserHash(account, password, salt): 261 | # type: (str, str, str) -> int 262 | """Compute a hash from the account, password, and salt.""" 263 | md = hashlib.sha1() 264 | userInfo = '%s:%s' % (account, password) 265 | md.update(userInfo.encode('latin-1')) 266 | hash1 = md.digest() 267 | md = hashlib.sha1() 268 | md.update(binascii.a2b_hex(salt)) 269 | md.update(hash1) 270 | 271 | return fromByteString(bytesToArray(md.digest())) 272 | 273 | @staticmethod 274 | def _computeScramble(clientPublicKey, serverPublicKey): 275 | # type: (int, int) -> int 276 | """Compute the scramble given the public keys.""" 277 | md = hashlib.sha1() 278 | md.update(toByteString(clientPublicKey)) 279 | md.update(toByteString(serverPublicKey)) 280 | 281 | return fromByteString(bytesToArray(md.digest())) 282 | 283 | def _getGroup(self): 284 | # type: () -> RemoteGroup 285 | """Return the RemoteGroup for this password.""" 286 | return self.__group 287 | 288 | 289 | class ClientPassword(RemotePassword): 290 | """Manage the client password.""" 291 | 292 | __privateKey = 0 293 | __publicKey = 0 294 | 295 | def genClientKey(self): 296 | # type: () -> str 297 | """Return the client key.""" 298 | group = self._getGroup() 299 | 300 | self.__privateKey = random.getrandbits(256) 301 | self.__publicKey = pow(group.getGenerator(), self.__privateKey, group.getPrime()) 302 | 303 | return toHex(self.__publicKey) 304 | 305 | def computeSessionKey(self, account, password, salt, serverKey): 306 | # type: (str, str, str, str) -> bytes 307 | """Compute the session key.""" 308 | serverPubKey = fromHex(serverKey) 309 | scramble = self._computeScramble(self.__publicKey, serverPubKey) 310 | 311 | group = self._getGroup() 312 | prime = group.getPrime() 313 | 314 | x = self._getUserHash(account, password, salt) 315 | gx = pow(group.getGenerator(), x, prime) 316 | kgx = (group.getK() * gx) % prime 317 | diff = (serverPubKey - kgx) % prime 318 | ux = (scramble * x) % prime 319 | aux = (self.__privateKey + ux) % prime 320 | 321 | sessionSecret = pow(diff, aux, prime) 322 | return toByteString(sessionSecret) 323 | 324 | 325 | class BaseCipher(object): 326 | """Base class for ciphers.""" 327 | 328 | name = 'invalid' 329 | keysize = 0 330 | 331 | def _convert_key(self, full): 332 | # type: (bytes) -> bytes 333 | """Convert a full key to the size needed for a given cipher.""" 334 | if self.keysize == hashlib.sha256().digest_size: 335 | md = hashlib.sha256() 336 | elif self.keysize == hashlib.sha1().digest_size: 337 | md = hashlib.sha1() 338 | elif self.keysize == hashlib.md5().digest_size: 339 | md = hashlib.md5() 340 | else: 341 | raise Exception("Invalid key size: %d" % (self.keysize)) 342 | md.update(full) 343 | return md.digest() 344 | 345 | def transform(self, data): 346 | # type: (bytes) -> bytes 347 | """Perform a byte by byte cipher transform on the input.""" 348 | raise NotImplementedError("Invalid cipher") 349 | 350 | 351 | class NoCipher(BaseCipher): 352 | """No cipher.""" 353 | 354 | name = 'None' 355 | 356 | def transform(self, data): 357 | # type: (bytes) -> bytes 358 | """:returns: the input data unchanged.""" 359 | return data 360 | 361 | 362 | class AESBaseCipher(BaseCipher): 363 | """An AES cipher object using cryptography.""" 364 | 365 | def __init__(self, encrypt, key, nonce): 366 | # type: (bool, bytes, bytes) -> None 367 | """Create an AES cipher using the given key and nonce. 368 | 369 | :param encrypt: True if encrypting, False if decrypting 370 | :param key: The key to initialize from. 371 | :param nonce: The nonce for the cipher or None to create it 372 | """ 373 | algo = algorithms.AES(self._convert_key(key)) 374 | cipher = Cipher(algo, mode=modes.CTR(nonce), backend=default_backend()) 375 | self.cipher = cipher.encryptor() if encrypt else cipher.decryptor() 376 | 377 | def transform(self, data): 378 | # type: (bytes) -> bytes 379 | """:returns: data transformed by the cipher.""" 380 | return self.cipher.update(data) 381 | 382 | 383 | class AES256Cipher(AESBaseCipher): 384 | """An AES-256 cipher object using cryptography.""" 385 | 386 | name = 'AES-256' 387 | keysize = int(256 / 8) 388 | 389 | 390 | class AES128Cipher(AESBaseCipher): 391 | """An AES-128 cipher object using cryptography.""" 392 | 393 | name = 'AES-128' 394 | keysize = int(128 / 8) 395 | 396 | 397 | class RC4CipherNuoDB(BaseCipher): 398 | """An RC4 cipher object using a native Python algorithm.""" 399 | 400 | name = 'RC4-local' 401 | keysize = int(160 / 8) 402 | 403 | def __init__(self, _, key, convert=True): 404 | # type: (bool, bytes, bool) -> None 405 | """Create an RC4 cipher using the given key. 406 | 407 | This uses a native Python implementation which is sloooooow. 408 | It will be used if you don't have cryptography installed. 409 | Encryption and decryption use the same algorithm. 410 | 411 | :param encrypt: True if encrypting, False if decrypting 412 | :param key: The cipher key. 413 | :param convert: If True hash the key 414 | """ 415 | super(RC4CipherNuoDB, self).__init__() 416 | 417 | self.__state = list(range(256)) 418 | self.__idx1 = 0 419 | self.__idx2 = 0 420 | 421 | state = self.__state 422 | 423 | if convert: 424 | key = self._convert_key(key) 425 | data = bytesToArray(key) 426 | 427 | sz = len(data) 428 | j = 0 429 | for i in range(256): 430 | val = data[i % sz] 431 | j = (j + state[i] + val) % 256 432 | state[i], state[j] = state[j], state[i] 433 | 434 | def transform(self, data): 435 | # type: (bytes) -> bytes 436 | """Perform a byte by byte RC4 transform on the stream. 437 | 438 | Python 2: 439 | automatically handles encoding bytes into an extended ASCII 440 | encoding [0,255] w/ 1 byte per character 441 | 442 | Python 3: 443 | bytes objects must be converted into extended ASCII, latin-1 uses 444 | the desired range of [0,255] 445 | 446 | For utf-8 strings (characters consisting of more than 1 byte) the 447 | values are broken into 1 byte sections and shifted. The RC4 stream 448 | cipher processes 1 byte at a time, as does ord when converting 449 | character values to integers. 450 | 451 | :param data: Data to be transformed. 452 | :returns: Transformed data. 453 | """ 454 | transformed = bytearray() 455 | state = self.__state 456 | 457 | for char in bytesToArray(data): 458 | self.__idx1 = (self.__idx1 + 1) % 256 459 | self.__idx2 = (self.__idx2 + state[self.__idx1]) % 256 460 | state[self.__idx1], state[self.__idx2] = state[self.__idx2], state[self.__idx1] 461 | cipherByte = char ^ state[(state[self.__idx1] + state[self.__idx2]) % 256] 462 | transformed.append(cipherByte) 463 | return bytes(transformed) 464 | 465 | 466 | class RC4CipherCryptography(BaseCipher): 467 | """An RC4 cipher object using cryptography.""" 468 | 469 | name = 'RC4' 470 | keysize = int(160 / 8) 471 | 472 | def __init__(self, encrypt, key, convert=True): 473 | # type: (bool, bytes, bool) -> None 474 | """Create an RC4 cipher using the given key. 475 | 476 | :param encrypt: True if encrypting, False if decrypting 477 | :param key: The key to initialize from. 478 | :param convert: If True hash the key 479 | """ 480 | if convert: 481 | key = self._convert_key(key) 482 | algo = ARC4(key) 483 | 484 | # There's a bug in older versions of mypy where they don't infer the 485 | # optionality of mode correctly. 486 | # https://github.com/pyca/cryptography/issues/9464 487 | cipher = Cipher(algo, mode=None, # type: ignore 488 | backend=default_backend()) 489 | self.cipher = cipher.encryptor() if encrypt else cipher.decryptor() 490 | 491 | def transform(self, data): 492 | # type: (bytes) -> bytes 493 | """:returns: data transformed by the cipher.""" 494 | return self.cipher.update(data) 495 | 496 | 497 | if arc4Imported: 498 | RC4Cipher = RC4CipherCryptography 499 | else: 500 | RC4Cipher = RC4CipherNuoDB # type: ignore 501 | -------------------------------------------------------------------------------- /pynuodb/cursor.py: -------------------------------------------------------------------------------- 1 | """A module for housing the Cursor class. 2 | 3 | (C) Copyright 2013-2025 Dassault Systemes SE. All Rights Reserved. 4 | 5 | This software is licensed under a BSD 3-Clause License. 6 | See the LICENSE file provided with this software. 7 | 8 | Exported Classes: 9 | 10 | Cursor -- Class for representing a database cursor. 11 | """ 12 | 13 | from collections import deque 14 | 15 | # pylint: disable=unused-import 16 | try: 17 | from typing import Any, Collection, Deque, Dict, Iterable, Mapping, List, Optional 18 | except ImportError: 19 | pass 20 | 21 | from .exception import Error, NotSupportedError, ProgrammingError 22 | 23 | from . import statement # pylint: disable=unused-import 24 | from . import encodedsession # pylint: disable=unused-import 25 | from . import result_set # pylint: disable=unused-import 26 | 27 | 28 | class Cursor(object): 29 | """A database cursor. 30 | 31 | Public Functions: 32 | close -- Closes the cursor into the database. 33 | callproc -- Currently not supported. 34 | execute -- Executes an SQL operation. 35 | executemany -- Executes the operation for each list of paramaters passed in. 36 | fetchone -- Fetches the first row of results generated by the previous execute. 37 | fetchmany -- Fetches the number of rows that are passed in. 38 | fetchall -- Fetches everything generated by the previous execute. 39 | nextset -- Currently not supported. 40 | setinputsizes -- Currently not supported. 41 | setoutputsize -- Currently not supported. 42 | """ 43 | 44 | description = None # type: Optional[List[Any]] 45 | 46 | _result_set = None # type: Optional[result_set.ResultSet] 47 | __query = None # type: Optional[str] 48 | 49 | @property 50 | def query(self): 51 | # type: () -> Optional[str] 52 | """Return the most recent query.""" 53 | return self.__query 54 | 55 | def __init__(self, session, cache_size): 56 | # type: (encodedsession.EncodedSession, int) -> None 57 | """Create a Cursor object. 58 | 59 | :param session: The session to use with this Cursor. 60 | """ 61 | self.session = session 62 | self._statement_cache = StatementCache(session, cache_size) 63 | self._result_set = None 64 | self.closed = False 65 | self.arraysize = 1 66 | self.rowcount = -1 67 | self.colcount = -1 68 | self.rownumber = 0 69 | 70 | def __iter__(self): 71 | # type: () -> Cursor 72 | return self 73 | 74 | def next(self): 75 | # type: () -> result_set.Row 76 | """Return the next row of results from the previous SQL operation.""" 77 | row = self.fetchone() 78 | if row is None: 79 | raise StopIteration 80 | return row 81 | 82 | def close(self): 83 | # type: () -> None 84 | """Close this cursor.""" 85 | self._check_closed() 86 | self._statement_cache.shutdown() 87 | self._close_result_set() 88 | self.closed = True 89 | 90 | def _check_closed(self): 91 | # type: () -> None 92 | """Check if the cursor is closed. 93 | 94 | :raises Error: If the cursor is closed. 95 | """ 96 | if self.closed: 97 | raise Error("cursor is closed") 98 | if self.session.closed: 99 | raise Error("connection is closed") 100 | 101 | def _close_result_set(self): 102 | # type: () -> None 103 | """Close current ResultSet on client and server side.""" 104 | if self._result_set: 105 | self.session.close_result_set(self._result_set) 106 | self._result_set = None 107 | 108 | def _reset(self): 109 | # type: () -> None 110 | """Reset the Cursor to a default state.""" 111 | self.description = None 112 | self.rowcount = -1 113 | self.colcount = -1 114 | self._close_result_set() 115 | 116 | def callproc(self, procname, parameters=None): # pylint: disable=no-self-use 117 | # type: (str, Optional[Mapping[str, str]]) -> None 118 | """Invoke a stored procedure. 119 | 120 | Currently not supported. 121 | """ 122 | if procname is not None or parameters is not None: 123 | raise NotSupportedError("Currently unsupported") 124 | 125 | def execute(self, operation, parameters=None): 126 | # type: (str, Optional[Collection[Any]]) -> None 127 | """Execute a SQL operation. 128 | 129 | :param operation: The SQL operation to be executed. 130 | :param parameters: Statement parameters. 131 | """ 132 | self._check_closed() 133 | self._reset() 134 | self.__query = operation 135 | 136 | if parameters is None: 137 | exec_result = self._execute(operation) 138 | else: 139 | exec_result = self._executeprepared(operation, parameters) 140 | 141 | self.rowcount = exec_result.row_count 142 | if exec_result.result > 0: 143 | self._result_set = self.session.fetch_result_set(exec_result.statement) 144 | self.description = self.session.fetch_result_set_description(self._result_set) 145 | 146 | # TODO: ??? 147 | if self.rowcount < 0: 148 | self.rowcount = -1 149 | self.rownumber = 0 150 | 151 | def _execute(self, operation): 152 | # type: (str) -> statement.ExecutionResult 153 | """Execute an operation without parameters. 154 | 155 | :param operation: SQL operation to execute. 156 | """ 157 | return self.session.execute_statement( 158 | self._statement_cache.get_statement(), operation) 159 | 160 | def _executeprepared(self, operation, parameters): 161 | # type: (str, Collection[result_set.Value]) -> statement.ExecutionResult 162 | """Execute an operation with parameters. 163 | 164 | :param operation: SQL operation to execute. 165 | :raises ProgrammingError: Incorrect number of parameters 166 | """ 167 | p_statement = self._statement_cache.get_prepared_statement(operation) 168 | if p_statement.parameter_count != len(parameters): 169 | raise ProgrammingError( 170 | "Incorrect number of parameters: expected %d, got %d" % 171 | (p_statement.parameter_count, len(parameters))) 172 | 173 | # Use handle to query 174 | return self.session.execute_prepared_statement(p_statement, parameters) 175 | 176 | def executemany(self, operation, seq_of_parameters): 177 | # type: (str, Collection[Collection[result_set.Value]]) -> List[int] 178 | """Execute the operation for each list of paramaters passed in.""" 179 | self._check_closed() 180 | p_statement = self._statement_cache.get_prepared_statement(operation) 181 | return self.session.execute_batch_prepared_statement( 182 | p_statement, seq_of_parameters) 183 | 184 | def fetchone(self): 185 | # type: () -> Optional[result_set.Row] 186 | """Return the next row of results from the previous SQL operation.""" 187 | self._check_closed() 188 | if self._result_set is None: 189 | raise Error("Previous execute did not produce any results or no call was issued yet") 190 | self.rownumber += 1 191 | if not self._result_set.is_complete(): 192 | self.session.fetch_result_set_next(self._result_set) 193 | return self._result_set.fetchone() 194 | 195 | def fetchmany(self, size=None): 196 | # type: (Optional[int]) -> List[result_set.Row] 197 | """Return SIZE rows from the previous SQL operation. 198 | 199 | If size is None, uses the default size for this Cursor. 200 | """ 201 | self._check_closed() 202 | 203 | if size is None: 204 | size = self.arraysize 205 | 206 | fetched_rows = [] 207 | num_fetched_rows = 0 208 | while num_fetched_rows < size: 209 | row = self.fetchone() 210 | if row is None: 211 | break 212 | else: 213 | fetched_rows.append(row) 214 | num_fetched_rows += 1 215 | return fetched_rows 216 | 217 | def fetchall(self): 218 | # type: () -> List[result_set.Row] 219 | """Return all rows generated by the previous SQL operation.""" 220 | self._check_closed() 221 | 222 | fetched_rows = [] 223 | while True: 224 | row = self.fetchone() 225 | if row is None: 226 | break 227 | else: 228 | fetched_rows.append(row) 229 | return fetched_rows 230 | 231 | def nextset(self): # pylint: disable=no-self-use 232 | # type: () -> None 233 | """Not supported.""" 234 | raise NotSupportedError("Currently unsupported") 235 | 236 | def setinputsizes(self, sizes): 237 | # type: (int) -> None 238 | """Not supported.""" 239 | pass 240 | 241 | def setoutputsize(self, size, column=None): 242 | # type: (int, Optional[int]) -> None 243 | """Not supported.""" 244 | pass 245 | 246 | 247 | class StatementCache(object): 248 | """Keep a cache of prepared statements.""" 249 | 250 | def __init__(self, session, prepared_statement_cache_size): 251 | # type: (encodedsession.EncodedSession, int) -> None 252 | self._session = session 253 | self._statement = self._session.create_statement() 254 | self._ps_cache = dict() # type: Dict[str, statement.PreparedStatement] 255 | self._ps_key_queue = deque() # type: Deque[str] 256 | self._ps_cache_size = prepared_statement_cache_size 257 | 258 | def get_statement(self): 259 | # type; () -> statement.Statement 260 | """Return the Statement object for this cache.""" 261 | return self._statement 262 | 263 | def get_prepared_statement(self, query): 264 | # type: (str) -> statement.PreparedStatement 265 | """Return a PreparedStatement for the provided query. 266 | 267 | If we don't have a cached PreparedStatement then create one and add it 268 | to the cache. If we do have one move it to the front of the queue and 269 | return it. 270 | :returns: A PreparedStatement for the given query. 271 | """ 272 | stmt = self._ps_cache.get(query) 273 | if stmt is not None: 274 | self._ps_key_queue.remove(query) 275 | self._ps_key_queue.append(query) 276 | return stmt 277 | 278 | stmt = self._session.create_prepared_statement(query) 279 | 280 | while len(self._ps_cache) >= self._ps_cache_size: 281 | lru_statement_key = self._ps_key_queue.popleft() 282 | statement_to_remove = self._ps_cache[lru_statement_key] 283 | self._session.close_statement(statement_to_remove) 284 | del self._ps_cache[lru_statement_key] 285 | 286 | self._ps_key_queue.append(query) 287 | self._ps_cache[query] = stmt 288 | 289 | return stmt 290 | 291 | def shutdown(self): 292 | # type: () -> None 293 | """Close the connection and clear the cursor cache.""" 294 | self._session.close_statement(self._statement) 295 | 296 | for key in self._ps_cache: 297 | statement_to_remove = self._ps_cache[key] 298 | self._session.close_statement(statement_to_remove) 299 | 300 | self._ps_cache.clear() 301 | self._ps_key_queue.clear() 302 | -------------------------------------------------------------------------------- /pynuodb/datatype.py: -------------------------------------------------------------------------------- 1 | """A module for housing the datatype classes. 2 | 3 | (C) Copyright 2013-2025 Dassault Systemes SE. All Rights Reserved. 4 | 5 | This software is licensed under a BSD 3-Clause License. 6 | See the LICENSE file provided with this software. 7 | 8 | Exported Classes: 9 | Binary -- Class for a Binary object 10 | 11 | Exported Functions: 12 | DateFromTicks -- Converts ticks to a Date object. 13 | TimeFromTicks -- Converts ticks to a Time object. 14 | TimestampFromTicks -- Converts ticks to a Timestamp object. 15 | DateToTicks -- Converts a Date object to ticks. 16 | TimeToTicks -- Converts a Time object to ticks. 17 | TimestampToTicks -- Converts a Timestamp object to ticks. 18 | TypeObjectFromNuodb -- Converts a Nuodb column type name to a TypeObject variable. 19 | 20 | TypeObject Variables: 21 | STRING -- TypeObject(str) 22 | BINARY -- TypeObject(str) 23 | NUMBER -- TypeObject(int, decimal.Decimal) 24 | DATETIME -- TypeObject(datetime.datetime, datetime.date, datetime.time) 25 | ROWID -- TypeObject() 26 | """ 27 | 28 | __all__ = ['Date', 'Time', 'Timestamp', 'DateFromTicks', 'TimeFromTicks', 29 | 'TimestampFromTicks', 'DateToTicks', 'TimeToTicks', 30 | 'TimestampToTicks', 'Binary', 'STRING', 'BINARY', 'NUMBER', 31 | 'DATETIME', 'ROWID', 'TypeObjectFromNuodb'] 32 | 33 | import sys 34 | import decimal 35 | import time 36 | 37 | from datetime import datetime as Timestamp, date as Date, time as Time 38 | from datetime import timedelta as TimeDelta 39 | 40 | try: 41 | from typing import Tuple, Union # pylint: disable=unused-import 42 | except ImportError: 43 | pass 44 | 45 | from .exception import DataError 46 | 47 | isP2 = sys.version[0] == '2' 48 | 49 | 50 | class Binary(bytes): 51 | """A binary string. 52 | 53 | If passed a string we assume it's encoded as LATIN-1, which ensures that 54 | the characters 0-255 are considered single-character sequences. 55 | """ 56 | 57 | def __new__(cls, data): 58 | # type: (Union[str, bytes, bytearray]) -> Binary 59 | # I can't figure out how to get mypy to be OK with this. 60 | if isinstance(data, bytearray): 61 | return bytes.__new__(cls, data) # type: ignore 62 | # In Python2 there's no distinction between str and bytes :( 63 | if isinstance(data, str) and not isP2: 64 | return bytes.__new__(cls, data.encode('latin-1')) # type: ignore 65 | return bytes.__new__(cls, data) # type: ignore 66 | 67 | def __str__(self): 68 | # type: () -> str 69 | # This is pretty terrible but it's what the old version did. 70 | # What does it really mean to run str(Binary)? That should probably 71 | # be illegal, but I'm sure lots of code does "%s" % (Binary(x)) or 72 | # the equivalent. In Python 3 we have to remove the 'b' prefix too. 73 | # I'll leave this for consideration at some future time. 74 | return repr(self)[1:-1] if isP2 else repr(self)[2:-1] 75 | 76 | @property 77 | def string(self): 78 | # type: () -> bytes 79 | """The old implementation of Binary provided this.""" 80 | return self 81 | 82 | 83 | def DateFromTicks(ticks): 84 | # type: (int) -> Date 85 | """Convert ticks to a Date object.""" 86 | return Date(*time.localtime(ticks)[:3]) 87 | 88 | 89 | def TimeFromTicks(ticks, micro=0): 90 | # type: (int, int) -> Time 91 | """Convert ticks to a Time object.""" 92 | return Time(*time.localtime(ticks)[3:6] + (micro,)) 93 | 94 | 95 | def TimestampFromTicks(ticks, micro=0): 96 | # type: (int, int) -> Timestamp 97 | """Convert ticks to a Timestamp object.""" 98 | return Timestamp(*time.localtime(ticks)[:6] + (micro,)) 99 | 100 | 101 | def DateToTicks(value): 102 | # type: (Date) -> int 103 | """Convert a Date object to ticks.""" 104 | timeStruct = Date(value.year, value.month, value.day).timetuple() 105 | try: 106 | return int(time.mktime(timeStruct)) 107 | except Exception: 108 | raise DataError("Year out of range") 109 | 110 | 111 | def TimeToTicks(value): 112 | # type: (Time) -> Tuple[int, int] 113 | """Convert a Time object to ticks.""" 114 | timeStruct = TimeDelta(hours=value.hour, minutes=value.minute, 115 | seconds=value.second, 116 | microseconds=value.microsecond) 117 | timeDec = decimal.Decimal(str(timeStruct.total_seconds())) 118 | return (int((timeDec + time.timezone) * 10**abs(timeDec.as_tuple()[2])), 119 | abs(timeDec.as_tuple()[2])) 120 | 121 | 122 | def TimestampToTicks(value): 123 | # type: (Timestamp) -> Tuple[int, int] 124 | """Convert a Timestamp object to ticks.""" 125 | timeStruct = Timestamp(value.year, value.month, value.day, value.hour, 126 | value.minute, value.second).timetuple() 127 | try: 128 | if not value.microsecond: 129 | return (int(time.mktime(timeStruct)), 0) 130 | micro = decimal.Decimal(value.microsecond) / decimal.Decimal(1000000) 131 | t1 = decimal.Decimal(int(time.mktime(timeStruct))) + micro 132 | tlen = len(str(micro)) - 2 133 | return (int(t1 * decimal.Decimal(int(10**tlen))), tlen) 134 | except Exception: 135 | raise DataError("Year out of range") 136 | 137 | 138 | class TypeObject(object): 139 | """A SQL type object.""" 140 | 141 | def __init__(self, *values): 142 | self.values = values 143 | 144 | def __cmp__(self, other): 145 | if other in self.values: 146 | return 0 147 | if other < self.values: 148 | return 1 149 | return -1 150 | 151 | 152 | STRING = TypeObject(str) 153 | BINARY = TypeObject(str) 154 | NUMBER = TypeObject(int, decimal.Decimal) 155 | DATETIME = TypeObject(Timestamp, Date, Time) 156 | ROWID = TypeObject() 157 | NULL = TypeObject(None) 158 | 159 | TYPEMAP = {"": NULL, 160 | "string": STRING, 161 | "char": STRING, 162 | "varchar": STRING, 163 | "smallint": NUMBER, 164 | "integer": NUMBER, 165 | "bigint": NUMBER, 166 | "float": NUMBER, 167 | "double": NUMBER, 168 | "date": DATETIME, 169 | "timestamp": DATETIME, 170 | "time": DATETIME, 171 | "clob": BINARY, 172 | "blob": BINARY, 173 | "numeric": NUMBER, 174 | "number": NUMBER, 175 | "bytes": BINARY, 176 | "binary": BINARY, 177 | "binary varying": BINARY, 178 | "boolean": NUMBER, 179 | "timestamp without time zone": DATETIME, 180 | "timestamp with time zone": DATETIME, 181 | "time without time zone": DATETIME, 182 | # Old types used by NuoDB <2.0.3 183 | "binarystring": BINARY, 184 | "binaryvaryingstring": BINARY, 185 | } 186 | 187 | 188 | def TypeObjectFromNuodb(nuodb_type_name): 189 | # type: (str) -> TypeObject 190 | """Return a TypeObject based on the supplied NuoDB column type name.""" 191 | name = nuodb_type_name.strip() 192 | obj = TYPEMAP.get(name) 193 | if obj is None: 194 | raise DataError('received unknown column type "%s"' % (name)) 195 | return obj 196 | -------------------------------------------------------------------------------- /pynuodb/exception.py: -------------------------------------------------------------------------------- 1 | """Classes containing the exceptions for reporting errors. 2 | 3 | (C) Copyright 2013-2023 Dassault Systemes SE. All Rights Reserved. 4 | 5 | This software is licensed under a BSD 3-Clause License. 6 | See the LICENSE file provided with this software. 7 | """ 8 | 9 | import sys 10 | 11 | try: 12 | from typing import Iterable, NoReturn # pylint: disable=unused-import 13 | except ImportError: 14 | pass 15 | 16 | from . import protocol 17 | 18 | __all__ = ['Warning', 'Error', 'InterfaceError', 'DatabaseError', 'BatchError', 19 | 'DataError', 'OperationalError', 'IntegrityError', 'InternalError', 20 | 'ProgrammingError', 'NotSupportedError', 'EndOfStream', 21 | 'db_error_handler'] 22 | 23 | isP2 = sys.version[0] == '2' 24 | 25 | 26 | # These exceptions are defined by PEP 249. 27 | # See the PEP for a fuller description of each one. 28 | 29 | if isP2: 30 | class Warning(StandardError): # type: ignore # pylint: disable=redefined-builtin 31 | """Raised for important warnings.""" 32 | 33 | pass 34 | 35 | class Error(StandardError): # type: ignore 36 | """The base class of all other error exceptions.""" 37 | 38 | pass 39 | else: 40 | # Mypy is not smart enough to realize we'll only define one set of classes 41 | # so disable type checking 42 | class Warning(Exception): # type: ignore # pylint: disable=redefined-builtin 43 | """Raised for important warnings.""" 44 | 45 | pass 46 | 47 | class Error(Exception): # type: ignore 48 | """The base class of all other error exceptions.""" 49 | 50 | pass 51 | 52 | 53 | class InterfaceError(Error): 54 | """Raised for errors that are related to the database interface.""" 55 | 56 | pass 57 | 58 | 59 | class DatabaseError(Error): 60 | """Raised for errors that are related to the database.""" 61 | 62 | pass 63 | 64 | 65 | class DataError(DatabaseError): 66 | """Raised for errors that are due to problems with the processed data.""" 67 | 68 | pass 69 | 70 | 71 | class OperationalError(DatabaseError): 72 | """Raised for errors that are related to the database's operation.""" 73 | 74 | pass 75 | 76 | 77 | class IntegrityError(DatabaseError): 78 | """Raised when the relational integrity of the database is affected.""" 79 | 80 | pass 81 | 82 | 83 | class InternalError(DatabaseError): 84 | """Raised when the database encounters an internal error.""" 85 | 86 | pass 87 | 88 | 89 | class ProgrammingError(DatabaseError): 90 | """Raised for programming errors.""" 91 | 92 | pass 93 | 94 | 95 | class NotSupportedError(DatabaseError): 96 | """Raised for using a method or database API which is not supported.""" 97 | 98 | pass 99 | 100 | 101 | # These exceptions are specific to the pynuodb implementation. 102 | 103 | class BatchError(DatabaseError): 104 | """Raised for errors encountered during batch operations. 105 | 106 | In addition tot the mess, a result for each operation in the batch is 107 | available in the results attribute. 108 | """ 109 | 110 | def __init__(self, value, results): 111 | # type: (str, Iterable[int]) -> None 112 | super(BatchError, self).__init__(value) 113 | self.results = results 114 | 115 | 116 | class EndOfStream(Exception): 117 | """End-of-stream means a network or protocol error.""" 118 | 119 | pass 120 | 121 | 122 | def db_error_handler(error_code, error_string): 123 | # type: (int, str) -> NoReturn 124 | """Raise the appropriate exception based on the error. 125 | 126 | :param error_code: The error code. 127 | :param error_string: Extra error information. 128 | :raises Error: The correct Error exception subclass. 129 | """ 130 | info = '%s: %s' % (protocol.lookup_code(error_code), error_string) 131 | 132 | if error_code in protocol.DATA_ERRORS: 133 | raise DataError(info) 134 | if error_code in protocol.OPERATIONAL_ERRORS: 135 | raise OperationalError(info) 136 | if error_code in protocol.INTEGRITY_ERRORS: 137 | raise IntegrityError(info) 138 | if error_code in protocol.INTERNAL_ERRORS: 139 | raise InternalError(info) 140 | if error_code in protocol.PROGRAMMING_ERRORS: 141 | raise ProgrammingError(info) 142 | if error_code in protocol.NOT_SUPPORTED_ERRORS: 143 | raise NotSupportedError(info) 144 | 145 | raise DatabaseError(info) 146 | -------------------------------------------------------------------------------- /pynuodb/protocol.py: -------------------------------------------------------------------------------- 1 | """Constants for the message protocol with the NuoDB database. 2 | 3 | (C) Copyright 2013-2025 Dassault Systemes SE. All Rights Reserved. 4 | 5 | This software is licensed under a BSD 3-Clause License. 6 | See the LICENSE file provided with this software. 7 | """ 8 | 9 | # pylint: disable=bad-whitespace 10 | 11 | # Data Types Encoding Rules 12 | NULL = 1 13 | TRUE = 2 14 | FALSE = 3 15 | BLOBID = 8 16 | CLOBID = 9 17 | INTMINUS10 = 10 18 | INTMINUS1 = 19 19 | INT0 = 20 20 | INT31 = 51 21 | INTLEN0 = 51 22 | INTLEN1 = 52 23 | INTLEN8 = 59 24 | SCALEDLEN0 = 60 25 | SCALEDLEN8 = 68 26 | UTF8COUNT0 = 68 27 | UTF8COUNT1 = 69 28 | UTF8COUNT4 = 72 29 | OPAQUECOUNT0 = 72 30 | OPAQUECOUNT1 = 73 31 | OPAQUECOUNT4 = 76 32 | DOUBLELEN0 = 77 33 | DOUBLELEN8 = 85 34 | MILLISECLEN0 = 86 # milliseconds since January 1, 1970 35 | MILLISECLEN8 = 94 36 | NANOSECLEN0 = 95 # nanoseconds since January 1, 1970 37 | NANOSECLEN8 = 103 38 | TIMELEN0 = 104 # milliseconds since midnight 39 | TIMELEN4 = 108 40 | UTF8LEN0 = 109 41 | UTF8LEN39 = 148 42 | OPAQUELEN0 = 149 43 | OPAQUELEN39 = 188 44 | BLOBLEN0 = 189 45 | BLOBLEN4 = 193 46 | CLOBLEN0 = 194 47 | CLOBLEN4 = 198 48 | SCALEDCOUNT1 = 199 49 | UUID = 200 50 | SCALEDDATELEN0 = 200 51 | SCALEDDATELEN1 = 201 52 | SCALEDDATELEN8 = 208 53 | SCALEDTIMELEN0 = 208 54 | SCALEDTIMELEN1 = 209 55 | SCALEDTIMELEN8 = 216 56 | SCALEDTIMESTAMPLEN0 = 216 57 | SCALEDTIMESTAMPLEN1 = 217 58 | SCALEDTIMESTAMPLEN8 = 224 59 | SCALEDCOUNT2 = 225 60 | LOBSTREAM0 = 226 61 | LOBSTREAM1 = 227 62 | LOBSTREAM4 = 230 63 | ARRAYLEN1 = 231 64 | ARRAYLEN8 = 238 65 | SCALEDCOUNT3 = 239 66 | DEBUGBARRIER = 240 67 | SCALEDTIMESTAMPNOTZ = 241 68 | 69 | # Protocol Messages 70 | FAILURE = 0 71 | OPENDATABASE = 3 72 | CLOSE = 5 73 | PREPARETRANSACTION = 6 74 | COMMITTRANSACTION = 7 75 | ROLLBACKTRANSACTION = 8 76 | PREPARE = 9 77 | CREATE = 11 78 | GETRESULTSET = 13 79 | CLOSESTATEMENT = 15 80 | EXECUTE = 18 81 | EXECUTEQUERY = 19 82 | EXECUTEUPDATE = 20 83 | SETCURSORNAME = 21 84 | EXECUTEPREPAREDSTATEMENT = 22 85 | EXECUTEPREPAREDQUERY = 23 86 | EXECUTEPREPAREDUPDATE = 24 87 | GETMETADATA = 26 88 | NEXT = 27 89 | CLOSERESULTSET = 28 90 | GET = 33 91 | GETCATALOGS = 34 92 | GETSCHEMAS = 35 93 | GETTABLES = 36 94 | GETCOLUMNS = 38 95 | GETPRIMARYKEYS = 40 96 | GETIMPORTEDKEYS = 41 97 | GETEXPORTEDKEYS = 42 98 | GETINDEXINFO = 43 99 | GETTABLETYPES = 44 100 | GETTYPEINFO = 45 101 | GETMORERESULTS = 46 102 | GETUPDATECOUNT = 47 103 | PING = 48 104 | GETTRIGGERS = 57 105 | GETAUTOCOMMIT = 59 106 | SETAUTOCOMMIT = 60 107 | ISREADONLY = 61 108 | SETREADONLY = 62 109 | GETTRANSACTIONISOLATION = 63 110 | SETTRANSACTIONISOLATION = 64 111 | GETSEQUENCEVALUE = 65 112 | ANALYZE = 70 113 | STATEMENTANALYZE = 71 114 | SETTRACEFLAGS = 72 115 | EXECUTEBATCH = 83 116 | EXECUTEBATCHPREPAREDSTATEMENT = 84 117 | GETPARAMETERMETADATA = 85 118 | AUTHENTICATION = 86 119 | GETGENERATEDKEYS = 87 120 | PREPAREKEYS = 88 121 | PREPAREKEYNAMES = 89 122 | PREPAREKEYIDS = 90 123 | EXECUTEKEYS = 91 124 | EXECUTEKEYNAMES = 92 125 | EXECUTEKEYIDS = 93 126 | EXECUTEUPDATEKEYS = 94 127 | EXECUTEUPDATEKEYNAMES = 95 128 | EXECUTEUPDATEKEYIDS = 96 129 | SETSAVEPOINT = 97 130 | RELEASESAVEPOINT = 98 131 | ROLLBACKTOSAVEPOINT = 99 132 | SUPPORTSTRANSACTIONISOLATION = 100 133 | GETCATALOG = 101 134 | GETCURRENTSCHEMA = 102 135 | PREPARECALL = 103 136 | EXECUTECALLABLESTATEMENT = 104 137 | SETQUERYTIMEOUT = 105 138 | GETPROCEDURES = 106 139 | GETPROCEDURECOLUMNS = 107 140 | GETSUPERTABLES = 108 141 | GETSUPERTYPES = 109 142 | GETFUNCTIONS = 110 143 | GETFUNCTIONCOLUMNS = 111 144 | GETTABLEPRIVILEGES = 112 145 | GETCOLUMNPRIVILEGES = 113 146 | GETCROSSREFERENCE = 114 147 | ALLPROCEDURESARECALLABLE = 115 148 | ALLTABLESARESELECTABLE = 116 149 | GETATTRIBUTES = 117 150 | GETUDTS = 118 151 | GETVERSIONCOLUMNS = 119 152 | GETLOBCHUNK = 120 153 | GETLASTSTATEMENTTIMEMICROS = 121 154 | AUTHORIZETYPESREQUEST = 122 155 | SETRESULTSETFETCHSIZE = 123 156 | SETSTATEMENTFETCHSIZE = 124 157 | RECOVERTRANSACTION = 125 158 | CREATESTATEMENTHOLD = 127 159 | PREPARESTATEMENTHOLD = 128 160 | PREPARECALLHOLD = 129 161 | SETCONNECTIONHOLDABILITY = 130 162 | GETCONNECTIONHOLDABILITY = 131 163 | GETRESULTSETHOLDABILITY = 132 164 | 165 | # Error code values 166 | SYNTAX_ERROR = -1 167 | FEATURE_NOT_YET_IMPLEMENTED = -2 168 | BUG_CHECK = -3 169 | COMPILE_ERROR = -4 170 | RUNTIME_ERROR = -5 171 | OCS_ERROR = -6 172 | NETWORK_ERROR = -7 173 | CONVERSION_ERROR = -8 174 | TRUNCATION_ERROR = -9 175 | CONNECTION_ERROR = -10 176 | DDL_ERROR = -11 177 | APPLICATION_ERROR = -12 178 | SECURITY_ERROR = -13 179 | DATABASE_CORRUPTION = -14 180 | VERSION_ERROR = -15 181 | LICENSE_ERROR = -16 182 | INTERNAL_ERROR = -17 183 | DEBUG_ERROR = -18 184 | LOST_BLOB = -19 185 | INCONSISTENT_BLOB = -20 186 | DELETED_BLOB = -21 187 | LOG_ERROR = -22 188 | DATABASE_DAMAGED = -23 189 | UPDATE_CONFLICT = -24 190 | NO_SUCH_TABLE = -25 191 | INDEX_OVERFLOW = -26 192 | UNIQUE_DUPLICATE = -27 193 | UNCOMMITTED_UPDATES = -28 194 | DEADLOCK = -29 195 | OUT_OF_MEMORY_ERROR = -30 196 | OUT_OF_RECORD_MEMORY_ERROR = -31 197 | LOCK_TIMEOUT = -32 198 | PLATFORM_ERROR = -36 199 | NO_SCHEMA = -37 200 | CONFIGURATION_ERROR = -38 201 | READ_ONLY_ERROR = -39 202 | NO_GENERATED_KEYS = -40 203 | THROWN_EXCEPTION = -41 204 | INVALID_TRANSACTION_ISOLATION = -42 205 | UNSUPPORTED_TRANSACTION_ISOLATION = -43 206 | INVALID_UTF8 = -44 207 | CONSTRAINT_ERROR = -45 208 | UPDATE_ERROR = -46 209 | I18N_ERROR = -47 210 | OPERATION_KILLED = -48 211 | INVALID_STATEMENT = -49 212 | IS_SHUTDOWN = -50 213 | IN_QUOTED_STRING = -51 214 | BATCH_UPDATE_ERROR = -52 215 | JAVA_ERROR = -53 216 | INVALID_FIELD = -54 217 | INVALID_INDEX_NULL = -55 218 | INVALID_OPERATION = -56 219 | INVALID_STATISTICS = -57 220 | INVALID_GENERATOR = -58 221 | OPERATION_TIMEOUT = -59 222 | NO_SUCH_INDEX = -60 223 | NO_SUCH_SEQUENCE = -61 224 | XAER_PROTO = -62 225 | UNKNOWN_ERROR = -63 226 | TRANSACTIONAL_LOCK_ERROR = -64 227 | TRANSACTION_UNKNOWN_STATE = -65 228 | LOCK_NOT_GRANTED = -66 229 | 230 | 231 | DATA_ERRORS = {COMPILE_ERROR, 232 | CONSTRAINT_ERROR, 233 | RUNTIME_ERROR, 234 | CONVERSION_ERROR, 235 | TRUNCATION_ERROR, 236 | VERSION_ERROR, 237 | INVALID_UTF8, 238 | I18N_ERROR} 239 | 240 | OPERATIONAL_ERRORS = {NETWORK_ERROR, 241 | DDL_ERROR, 242 | PLATFORM_ERROR, 243 | BATCH_UPDATE_ERROR, 244 | OPERATION_KILLED, 245 | INVALID_STATEMENT, 246 | INVALID_OPERATION} 247 | 248 | INTERNAL_ERRORS = {DATABASE_CORRUPTION, 249 | INTERNAL_ERROR, 250 | UPDATE_CONFLICT, 251 | DEADLOCK, 252 | IS_SHUTDOWN} 253 | 254 | INTEGRITY_ERRORS = {UNIQUE_DUPLICATE} 255 | 256 | PROGRAMMING_ERRORS = {SYNTAX_ERROR, 257 | CONNECTION_ERROR, 258 | APPLICATION_ERROR, 259 | SECURITY_ERROR, 260 | NO_SUCH_TABLE, 261 | NO_SCHEMA, 262 | CONFIGURATION_ERROR, 263 | READ_ONLY_ERROR, 264 | IN_QUOTED_STRING} 265 | 266 | NOT_SUPPORTED_ERRORS = {FEATURE_NOT_YET_IMPLEMENTED, 267 | UNSUPPORTED_TRANSACTION_ISOLATION} 268 | 269 | 270 | stringifyError = { 271 | SYNTAX_ERROR: 'SYNTAX_ERROR', 272 | FEATURE_NOT_YET_IMPLEMENTED: 'FEATURE_NOT_YET_IMPLEMENTED', 273 | BUG_CHECK: 'BUG_CHECK', 274 | COMPILE_ERROR: 'COMPILE_ERROR', 275 | RUNTIME_ERROR: 'RUNTIME_ERROR', 276 | OCS_ERROR: 'OCS_ERROR', 277 | NETWORK_ERROR: 'NETWORK_ERROR', 278 | CONVERSION_ERROR: 'CONVERSION_ERROR', 279 | TRUNCATION_ERROR: 'TRUNCATION_ERROR', 280 | CONNECTION_ERROR: 'CONNECTION_ERROR', 281 | DDL_ERROR: 'DDL_ERROR', 282 | APPLICATION_ERROR: 'APPLICATION_ERROR', 283 | SECURITY_ERROR: 'SECURITY_ERROR', 284 | DATABASE_CORRUPTION: 'DATABASE_CORRUPTION', 285 | VERSION_ERROR: 'VERSION_ERROR', 286 | LICENSE_ERROR: 'LICENSE_ERROR', 287 | INTERNAL_ERROR: 'INTERNAL_ERROR', 288 | DEBUG_ERROR: 'DEBUG_ERROR', 289 | LOST_BLOB: 'LOST_BLOB', 290 | INCONSISTENT_BLOB: 'INCONSISTENT_BLOB', 291 | DELETED_BLOB: 'DELETED_BLOB', 292 | LOG_ERROR: 'LOG_ERROR', 293 | DATABASE_DAMAGED: 'DATABASE_DAMAGED', 294 | UPDATE_CONFLICT: 'UPDATE_CONFLICT', 295 | NO_SUCH_TABLE: 'NO_SUCH_TABLE', 296 | INDEX_OVERFLOW: 'INDEX_OVERFLOW', 297 | UNIQUE_DUPLICATE: 'UNIQUE_DUPLICATE', 298 | UNCOMMITTED_UPDATES: 'UNCOMMITTED_UPDATES', 299 | DEADLOCK: 'DEADLOCK', 300 | OUT_OF_MEMORY_ERROR: 'OUT_OF_MEMORY_ERROR', 301 | OUT_OF_RECORD_MEMORY_ERROR: 'OUT_OF_RECORD_MEMORY_ERROR', 302 | LOCK_TIMEOUT: 'LOCK_TIMEOUT', 303 | PLATFORM_ERROR: 'PLATFORM_ERROR', 304 | NO_SCHEMA: 'NO_SCHEMA', 305 | CONFIGURATION_ERROR: 'CONFIGURATION_ERROR', 306 | READ_ONLY_ERROR: 'READ_ONLY_ERROR', 307 | NO_GENERATED_KEYS: 'NO_GENERATED_KEYS', 308 | THROWN_EXCEPTION: 'THROWN_EXCEPTION', 309 | INVALID_TRANSACTION_ISOLATION: 'INVALID_TRANSACTION_ISOLATION', 310 | UNSUPPORTED_TRANSACTION_ISOLATION: 'UNSUPPORTED_TRANSACTION_ISOLATION', 311 | INVALID_UTF8: 'INVALID_UTF8', 312 | CONSTRAINT_ERROR: 'CONSTRAINT_ERROR', 313 | UPDATE_ERROR: 'UPDATE_ERROR', 314 | I18N_ERROR: 'I18N_ERROR', 315 | OPERATION_KILLED: 'OPERATION_KILLED', 316 | INVALID_STATEMENT: 'INVALID_STATEMENT', 317 | IS_SHUTDOWN: 'IS_SHUTDOWN', 318 | IN_QUOTED_STRING: 'IN_QUOTED_STRING', 319 | BATCH_UPDATE_ERROR: 'BATCH_UPDATE_ERROR', 320 | JAVA_ERROR: 'JAVA_ERROR', 321 | INVALID_FIELD: 'INVALID_FIELD', 322 | INVALID_INDEX_NULL: 'INVALID_INDEX_NULL', 323 | INVALID_OPERATION: 'INVALID_OPERATION', 324 | INVALID_STATISTICS: 'INVALID_STATISTICS', 325 | INVALID_GENERATOR: 'INVALID_GENERATOR', 326 | OPERATION_TIMEOUT: 'OPERATION_TIMEOUT', 327 | NO_SUCH_INDEX: 'NO_SUCH_INDEX', 328 | NO_SUCH_SEQUENCE: 'NO_SUCH_SEQUENCE', 329 | XAER_PROTO: 'XAER_PROTO', 330 | UNKNOWN_ERROR: 'UNKNOWN_ERROR', 331 | TRANSACTIONAL_LOCK_ERROR: 'TRANSACTIONAL_LOCK_ERROR', 332 | TRANSACTION_UNKNOWN_STATE: 'TRANSACTION_UNKNOWN_STATE', 333 | LOCK_NOT_GRANTED: 'LOCK_NOT_GRANTED' 334 | } 335 | 336 | 337 | def lookup_code(error_code): 338 | # type: (int) -> str 339 | """Return a string-ified version of an error code.""" 340 | return stringifyError.get(error_code, '[UNKNOWN ERROR CODE]') 341 | 342 | 343 | # 344 | # NuoDB Client-Server Features 345 | # 346 | 347 | NORMALIZED_DATES = 10 348 | ARBITRARY_DECIMAL = 11 349 | PREPARE_CALL = 12 350 | SET_QUERY_TIMEOUT = 12 351 | MORE_JDBC = 13 352 | STRING_CHANGE = 14 353 | SEND_CONNID_TO_CLIENT = 15 354 | USE_ACTUAL_DECLARED_TYPE = 15 355 | GET_TABLE_PRIVILEGES = 16 356 | GET_CROSS_REFERENCE = 16 357 | SEND_EFFECTIVE_PLATFORM_VERSION_TO_CLIENT = 16 358 | LAST_COMMIT_INFO = 17 359 | JDBC_METADATA_UPDATES = 18 360 | LOB_STREAMING = 18 361 | SERVER_TIMING = 19 362 | STORED_PROC_ARRAY_ARGS = 19 363 | OPERATION_TIMEOUT_ERROR = 19 364 | SET_FETCH_SIZE = 19 365 | XA_TRANSACTIONS = 19 366 | BIGINT_ENCODE_VER3 = 20 367 | SEND_PREPARE_STMT_RESULT_SET_METADATA_TO_CLIENT = 21 368 | DDL_NOT_AUTOCOMMITTED = 22 369 | MULTI_CIPHER = 23 370 | CURSOR_HOLDABILITY = 24 371 | TIMESTAMP_WITHOUT_TZ = 25 372 | PREPARE_AND_EXECUTE_TOGETHER = 26 373 | 374 | # The newest feature this driver supports. 375 | # The server will negotiate the highest compatible version. 376 | CURRENT_PROTOCOL_MAJOR = 1 377 | CURRENT_PROTOCOL_VERSION = CURSOR_HOLDABILITY 378 | AUTH_TEST_STR = 'Success!' 379 | -------------------------------------------------------------------------------- /pynuodb/result_set.py: -------------------------------------------------------------------------------- 1 | """NuoDB Python Driver result set. 2 | 3 | (C) Copyright 2013-2023 Dassault Systemes SE. All Rights Reserved. 4 | 5 | This software is licensed under a BSD 3-Clause License. 6 | See the LICENSE file provided with this software. 7 | """ 8 | 9 | try: 10 | from typing import Any, List, Optional, Tuple # pylint: disable=unused-import 11 | Value = Any 12 | Row = Tuple[Value, ...] 13 | except ImportError: 14 | pass 15 | 16 | 17 | class ResultSet(object): 18 | """Manage a SQL result set.""" 19 | 20 | def __init__(self, handle, col_count, initial_results, complete): 21 | # type: (int, int, List[Row], bool) -> None 22 | """Create a ResultSet object. 23 | 24 | :param handle: Connection handle. 25 | :param col_count: Column count. 26 | :param initial_results: Initial results for this set. 27 | :param complete: True if the result set is complete. 28 | """ 29 | self.handle = handle 30 | self.col_count = col_count 31 | self.results = initial_results 32 | self.results_idx = 0 33 | self.complete = complete 34 | 35 | def clear_results(self): 36 | # type: () -> None 37 | """Clear the result set.""" 38 | del self.results[:] 39 | self.results_idx = 0 40 | 41 | def add_row(self, row): 42 | # type: (Row) -> None 43 | """Add a new row to the result set.""" 44 | self.results.append(row) 45 | 46 | def is_complete(self): 47 | # type: () -> bool 48 | """Return True if the result set is complete.""" 49 | return self.complete or self.results_idx != len(self.results) 50 | 51 | def fetchone(self): 52 | # type: () -> Optional[Row] 53 | """Return the next row in the result set. 54 | 55 | :returns: The next row, or None if there are no more. 56 | """ 57 | if self.results_idx == len(self.results): 58 | return None 59 | 60 | res = self.results[self.results_idx] 61 | self.results_idx += 1 62 | return res 63 | -------------------------------------------------------------------------------- /pynuodb/session.py: -------------------------------------------------------------------------------- 1 | """Establish and manage a SQL session with a NuoDB database. 2 | 3 | (C) Copyright 2013-2025 Dassault Systemes SE. All Rights Reserved. 4 | 5 | This software is licensed under a BSD 3-Clause License. 6 | See the LICENSE file provided with this software. 7 | 8 | 9 | This module abstracts the common functionaliy needed to establish a session 10 | with an engine. It separates incoming and outgoing stream handling, with 11 | optional encryption, and correctly encodes and re-assembles messages based on 12 | their legnth. 13 | """ 14 | 15 | __all__ = ["checkForError", "SessionException", "Session"] 16 | 17 | import socket 18 | import struct 19 | import sys 20 | from ipaddress import ip_address 21 | import xml.etree.ElementTree as ET 22 | 23 | try: 24 | from urllib.parse import urlparse 25 | except ImportError: 26 | from urlparse import urlparse # type: ignore 27 | 28 | try: 29 | from typing import Dict, Generator, Iterable, Mapping # pylint: disable=unused-import 30 | from typing import Optional, Tuple, Union # pylint: disable=unused-import 31 | except ImportError: 32 | pass 33 | 34 | from .exception import Error, OperationalError, InterfaceError 35 | 36 | from . import crypt 37 | 38 | isP2 = sys.version[0] == '2' 39 | 40 | NUODB_PORT = 48004 41 | 42 | 43 | class SessionException(OperationalError): # pylint: disable=too-many-ancestors 44 | """Raised for problems encountered with the network session. 45 | 46 | It's unfortunate that we invented this exception, but it may be widely 47 | used now. Make it a subclass of the OperationalError exception. 48 | """ 49 | 50 | pass 51 | 52 | 53 | def checkForError(message): 54 | # type: (str) -> None 55 | """Check a result XML string for errors. 56 | 57 | :param message: The message to be checked. 58 | :raises ET.ParseError: If the message is invalid XML. 59 | :raises SessionException: If the message is an error result. 60 | """ 61 | root = ET.fromstring(message) 62 | if root.tag == "Error": 63 | raise SessionException(root.get("text", "Unknown Error")) 64 | 65 | 66 | def strToBool(s): 67 | # type: (str) -> bool 68 | """Convert a database boolean value to a Python boolean. 69 | 70 | :param s: Value to convert 71 | :returns: True if the value is true, False if it's false 72 | :raises ValueError: If the value is not a valid boolean string. 73 | """ 74 | if s.lower() == 'true': 75 | return True 76 | elif s.lower() == 'false': 77 | return False 78 | raise ValueError('"%s" is not a valid boolean string' % s) 79 | 80 | 81 | def xmlToString(root): 82 | # type: (ET.Element) -> str 83 | """Convert an XML Element to a str.""" 84 | return ET.tostring(root, encoding='utf-8' if isP2 else 'unicode') 85 | 86 | 87 | class Session(object): 88 | """A NuoDB service session (either AP or Engine).""" 89 | 90 | __SERVICE_CONN = "" 91 | __SERVICE_REQ = "" 92 | __AUTH_REQ = "" 93 | __SRP_REQ = '' 94 | 95 | __isTLSEncrypted = False 96 | __cipherOut = None # type: crypt.BaseCipher 97 | __cipherIn = None # type: crypt.BaseCipher 98 | 99 | __port = NUODB_PORT # type: int 100 | __sock = None # type: Optional[socket.socket] 101 | 102 | @property 103 | def _sock(self): 104 | # type: () -> socket.socket 105 | """Return the socket: raise if it's closed.""" 106 | sock = self.__sock 107 | if sock is None: 108 | raise SessionException("Session is closed") 109 | return sock 110 | 111 | def __init__(self, host, # type: str 112 | port=None, # type: Optional[int] 113 | service="SQL2", # type: str 114 | timeout=None, # type: Optional[float] 115 | connect_timeout=None, # type: Optional[float] 116 | read_timeout=None, # type: Optional[float] 117 | options=None # type: Optional[Mapping[str, str]] 118 | ): 119 | # type: (...) -> None 120 | if options is None: 121 | options = {} 122 | 123 | self.__address, _port, ver = self._parse_addr(host, options.get('ipVersion')) 124 | if port is not None: 125 | self.__port = port 126 | elif _port is not None: 127 | self.__port = _port 128 | 129 | af = socket.AF_INET 130 | if ver == 6: 131 | af = socket.AF_INET6 132 | 133 | # for backwards-compatibility, set connect and read timeout to 134 | # `timeout` if either is not specified 135 | if connect_timeout is None: 136 | connect_timeout = timeout 137 | if read_timeout is None: 138 | read_timeout = timeout 139 | 140 | self.__service = service 141 | 142 | self._open_socket(connect_timeout, self.__address, self.__port, af, read_timeout) 143 | 144 | if options.get('trustStore') is not None: 145 | # We have to have a trustStore parameter to enable TLS 146 | try: 147 | self.establish_secure_tls_connection(options) 148 | except socket.error: 149 | if strToBool(options.get('allowSRPFallback', "False")): 150 | # fall back to SRP, do not attempt to TLS handshake 151 | self.close() 152 | self._open_socket(connect_timeout, self.__address, 153 | self.__port, af, read_timeout) 154 | else: 155 | raise 156 | 157 | @staticmethod 158 | def session_options(options): 159 | # type: (Optional[Mapping[str, str]]) -> Tuple[Dict[str, str], Dict[str, str]] 160 | """Split into connection parameters and session options. 161 | 162 | Connection parameters are passed to the SQL server to control the 163 | connection. Session options are not sent to the SQL server, and 164 | instead control the local session. 165 | 166 | :return: A tuple of (connection parameters, session options). 167 | """ 168 | opts = ['password', 'user', 'ipVersion', 'direct', 'allowSRPFallback', 169 | 'trustStore', 'sslVersion', 'verifyHostname'] 170 | session = {} 171 | parameters = {} 172 | if options: 173 | for key, val in options.items(): 174 | if key in opts: 175 | session[key] = val 176 | else: 177 | parameters[key] = val 178 | return parameters, session 179 | 180 | @staticmethod 181 | def _to_ipaddr(addr): 182 | # type: (str) -> Tuple[str, int] 183 | if isP2 and not isinstance(addr, unicode): # type: ignore 184 | ipaddr = ip_address(unicode(addr, 'utf_8')) # type: ignore 185 | else: 186 | ipaddr = ip_address(addr) 187 | return (str(ipaddr), ipaddr.version) 188 | 189 | def _parse_addr(self, addr, ipver): 190 | # type: (str, Optional[str]) -> Tuple[str, Optional[int], int] 191 | port = None 192 | try: 193 | # v4/v6 addr w/o port e.g. 192.168.1.1, 2001:3200:3200::10 194 | ip, ver = self._to_ipaddr(addr) 195 | except ValueError: 196 | # v4/v6 addr w/port e.g. 192.168.1.1:53, [2001::10]:53 197 | parsed = urlparse('//{}'.format(addr)) 198 | if parsed.hostname is None: 199 | raise InterfaceError("Invalid Host/IP Address format: %s" % (addr)) 200 | try: 201 | ip, ver = self._to_ipaddr(parsed.hostname) 202 | port = parsed.port 203 | except ValueError: 204 | parts = addr.split(":") 205 | if len(parts) == 1: 206 | # hostname w/o port e.g. ad0 207 | ip = addr 208 | elif len(parts) == 2: 209 | # hostname with port e.g. ad0:53 210 | ip = parts[0] 211 | try: 212 | port = int(parts[1]) 213 | except ValueError: 214 | raise InterfaceError("Invalid Host/IP Address Format %s" % addr) 215 | else: 216 | # failed 217 | raise InterfaceError("Invalid Host/IP Address Format %s" % addr) 218 | 219 | # select v6/v4 for hostname based on user option 220 | ver = 4 221 | if ipver == 'v6': 222 | ver = 6 223 | 224 | return ip, port, ver 225 | 226 | def _open_socket(self, connect_timeout, host, port, af, read_timeout): 227 | # type: (Optional[float], str, int, int, Optional[float]) -> None 228 | assert self.__sock is None, "Open called with already open socket" 229 | self.__sock = socket.socket(af, socket.SOCK_STREAM) 230 | # disable Nagle's algorithm 231 | self.__sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 232 | # separate connect and read timeout; we do not necessarily want to 233 | # close out connection if reads block for a long time, because it could 234 | # take a while for the server to generate data to send 235 | self.__sock.settimeout(connect_timeout) 236 | self.__sock.connect((host, port)) 237 | self.__sock.settimeout(read_timeout) 238 | 239 | def establish_secure_tls_connection(self, options): 240 | # type: (Mapping[str, str]) -> None 241 | """Establish a TLS connection to the service. 242 | 243 | :raises: RuntimeError if the Python SSL module is not available. 244 | """ 245 | try: 246 | import ssl 247 | except ImportError: 248 | raise RuntimeError("SSL requested but the ssl module not available") 249 | 250 | sslcontext = ssl.SSLContext(int(options.get('sslVersion', ssl.PROTOCOL_TLSv1_2))) 251 | sslcontext.options |= ssl.OP_NO_SSLv2 252 | sslcontext.options |= ssl.OP_NO_SSLv3 253 | sslcontext.verify_mode = ssl.CERT_REQUIRED 254 | sslcontext.check_hostname = strToBool(options.get('verifyHostname', "True")) 255 | 256 | sslcontext.load_verify_locations(options['trustStore']) 257 | 258 | self.__sock = sslcontext.wrap_socket(self._sock, server_hostname=self.__address) 259 | self.__isTLSEncrypted = True 260 | 261 | @property 262 | def tls_encrypted(self): 263 | # type: () -> bool 264 | """Return True if the session is encrypted with TLS.""" 265 | return self.__isTLSEncrypted 266 | 267 | @property 268 | def address(self): 269 | # type: () -> str 270 | """Return the address of the service.""" 271 | return self.__address 272 | 273 | @property 274 | def port(self): 275 | # type: () -> int 276 | """Return the port of the service.""" 277 | return self.__port 278 | 279 | @property 280 | def cipher_name(self): 281 | # type: () -> str 282 | """Return the name of the cipher for the current session. 283 | 284 | If using TLS this returns '' (see the tls_encrypted property). 285 | """ 286 | return self.__cipherOut.name if self.__cipherOut else '' 287 | 288 | def authorize(self, account, dbpassword, ciphers=None, cipher=None): 289 | # type: (str, str, Optional[str], Optional[str]) -> None 290 | """Authorize this session. 291 | 292 | You can only use this if you know the database password, and this is 293 | only available from the AP. 294 | :param account: Username 295 | :param dbpassword: Database connection password 296 | :param ciphers: Comma-separated list of acceptable ciphers 297 | :param cipher: Backward-compat only: do not use 298 | """ 299 | req = Session.__AUTH_REQ % (self.__service) 300 | self.send(req.encode()) 301 | 302 | if ciphers is None: 303 | ciphers = crypt.get_ciphers() if cipher is None else cipher 304 | 305 | cp = crypt.ClientPassword() 306 | key = cp.genClientKey() 307 | req = Session.__SRP_REQ % (key, ciphers, account) 308 | response = self.__sendAndReceive(req.encode()) 309 | 310 | try: 311 | root = ET.fromstring(response.decode()) 312 | if root.tag != "SRPResponse": 313 | raise InterfaceError("Request for authorization was denied: %s" 314 | % (xmlToString(root))) 315 | 316 | salt = root.get("Salt") 317 | if salt is None: 318 | raise SessionException("Malformed authorization response (salt)") 319 | serverKey = root.get("ServerKey") 320 | if serverKey is None: 321 | raise SessionException("Malformed authorization response (server key)") 322 | 323 | chosenCipher = root.get("Cipher", "RC4") 324 | 325 | # We're the client, so the server's incoming IV is our outgoing 326 | # and vice versa. 327 | incomingIV = crypt.hexstrToBytes(root.get("OutgoingIV")) 328 | outgoingIV = crypt.hexstrToBytes(root.get("IncomingIV")) 329 | 330 | self._setup_auth(account, dbpassword, chosenCipher, serverKey, 331 | salt, cp, incomingIV, outgoingIV) 332 | 333 | verifyMessage = self.recv() 334 | if verifyMessage is None: 335 | raise SessionException("Failed to establish session (no verification)") 336 | try: 337 | root = ET.fromstring(verifyMessage.decode()) 338 | except Exception as e: 339 | raise SessionException("Failed to establish session with password: " + str(e)) 340 | 341 | if root.tag != "PasswordVerify": 342 | raise SessionException("Unexpected verification response: " + root.tag) 343 | except Error: 344 | self.close() 345 | raise 346 | 347 | self.send(verifyMessage) 348 | 349 | def _setup_auth(self, username, # type: str 350 | password, # type: str 351 | cipher, # type: str 352 | serverKey, # type: str 353 | salt, # type: str 354 | cp, # type: crypt.ClientPassword 355 | incomingIV, # type: Optional[bytes] 356 | outgoingIV # type: Optional[bytes] 357 | ): 358 | # type: (...) -> None 359 | sessionKey = cp.computeSessionKey(username, password, salt, serverKey) 360 | 361 | if cipher == 'AES-256-CTR': 362 | # Pacify mypy 363 | assert incomingIV 364 | assert outgoingIV 365 | self.__cipherIn = crypt.AES256Cipher(False, sessionKey, incomingIV) 366 | self.__cipherOut = crypt.AES256Cipher(True, sessionKey, outgoingIV) 367 | elif cipher == 'AES-128-CTR': 368 | # Pacify mypy 369 | assert incomingIV 370 | assert outgoingIV 371 | self.__cipherIn = crypt.AES128Cipher(False, sessionKey, incomingIV) 372 | self.__cipherOut = crypt.AES128Cipher(True, sessionKey, outgoingIV) 373 | elif cipher == 'RC4': 374 | self.__cipherIn = crypt.RC4Cipher(False, sessionKey) 375 | self.__cipherOut = crypt.RC4Cipher(True, sessionKey) 376 | elif cipher == 'None': 377 | self.__cipherIn = crypt.NoCipher() 378 | self.__cipherOut = crypt.NoCipher() 379 | else: 380 | raise InterfaceError("Server requests unknown cipher %s" % (cipher)) 381 | 382 | def doConnect(self, attributes=None, # type: Optional[Mapping[str, str]] 383 | text=None, # type: Optional[str] 384 | children=None # type: Optional[Iterable[ET.Element]] 385 | ): 386 | # type: (...) -> None 387 | """Connect to the service.""" 388 | connectStr = self.__constructServiceMessage( 389 | Session.__SERVICE_CONN, attributes, text, children) 390 | 391 | try: 392 | self.send(connectStr.encode()) 393 | except Exception: 394 | self.close() 395 | raise 396 | 397 | def doRequest(self, attributes=None, # type: Optional[Dict[str, str]] 398 | text=None, # type: Optional[str] 399 | children=None # type: Optional[Iterable[ET.Element]] 400 | ): 401 | # type: (...) -> str 402 | """Ask the service to execute a request and return the response. 403 | 404 | Issues the request, closes the session and returns the response 405 | string, or raises an exeption if the session fails or the response is 406 | an error. 407 | """ 408 | requestStr = self.__constructServiceMessage( 409 | Session.__SERVICE_REQ, attributes, text, children) 410 | 411 | try: 412 | response = self.__sendAndReceive(requestStr.encode()).decode() 413 | checkForError(response) 414 | return response 415 | finally: 416 | self.close() 417 | 418 | def __constructServiceMessage(self, 419 | template, # type: str 420 | attrs, # type: Optional[Mapping[str, str]] 421 | text, # type: Optional[str] 422 | children # type: Optional[Iterable[ET.Element]] 423 | ): 424 | # type: (...) -> str 425 | """Create an XML service message and return it.""" 426 | attributeString = "" 427 | if attrs: 428 | for (key, value) in attrs.items(): 429 | attributeString += ' %s="%s"' % (key, value) 430 | 431 | message = template % (self.__service, attributeString) 432 | 433 | if children or text: 434 | root = ET.fromstring(message) 435 | 436 | if text: 437 | root.text = text 438 | 439 | if children: 440 | for child in children: 441 | root.append(child) 442 | 443 | message = xmlToString(root) 444 | 445 | return message 446 | 447 | def send(self, message): 448 | # type: (Union[str, bytes, bytearray]) -> None 449 | """Send an encoded message to the server over the socket. 450 | 451 | The message to be sent is either already-encoded bytes or bytearray, 452 | or it's a UTF-8 str. 453 | """ 454 | sock = self._sock 455 | 456 | if isinstance(message, bytearray): 457 | data = bytes(message) 458 | elif isinstance(message, bytes) or isP2: 459 | data = message # type: ignore 460 | elif isinstance(message, str): 461 | data = message.encode('utf-8') 462 | else: 463 | raise SessionException("Invalid message type: %s" % (type(message))) 464 | 465 | if self.__cipherOut: 466 | data = self.__cipherOut.transform(data) 467 | 468 | lenbuf = struct.pack("!I", len(data)) 469 | # It should be possible to send the length followed by the data so we 470 | # don't have to reallocate this entire buffer, but it is unreliable. 471 | buf = lenbuf + data 472 | view = memoryview(buf) 473 | end = len(buf) 474 | cur = 0 475 | 476 | try: 477 | while cur < end: 478 | cur += sock.send(view[cur:]) 479 | except Exception: 480 | self.close() 481 | raise 482 | 483 | def recv(self, timeout=None): 484 | # type: (Optional[float]) -> Optional[bytes] 485 | """Pull the next message from the socket. 486 | 487 | If timeout is None, wait forever (until read_timeout, if set). 488 | If timeout is a float, then set this timeout for this recv(). 489 | On timeout, return None but do not close the connection. 490 | """ 491 | try: 492 | # We only wait on timeout to read the header. Once we read 493 | # a header we'll wait as long as it takes to read the data. 494 | lengthHeader = self.__readFully(4, timeout=timeout) 495 | if lengthHeader is None: 496 | # This can only happen if the recv timed out 497 | return None 498 | msgLength = int(struct.unpack("!I", lengthHeader)[0]) 499 | msg = self.__readFully(msgLength) 500 | except Exception: 501 | self.close() 502 | raise 503 | 504 | if msg is None: 505 | # This can't happen because we don't set a timeout above 506 | raise RuntimeError("Session.recv read no data!") 507 | 508 | if self.__cipherIn: 509 | msg = self.__cipherIn.transform(msg) 510 | 511 | return msg 512 | 513 | def __readFully(self, msgLength, timeout=None): 514 | # type: (int, Optional[float]) -> Optional[bytes] 515 | """Pull the next complete message from the socket.""" 516 | sock = self._sock 517 | msg = bytearray() 518 | old_tmout = sock.gettimeout() 519 | while msgLength > 0: 520 | if timeout is not None: 521 | # It's a little wrong that this timeout applies to each recv() 522 | # instead of to the entire operation; however we only use this 523 | # when reading the header which will always be read in one 524 | # pass anyway. 525 | sock.settimeout(timeout) 526 | try: 527 | received = sock.recv(msgLength) 528 | except socket.timeout: 529 | return None 530 | except IOError as e: 531 | raise SessionException( 532 | "Session closed while receiving: network error %s: %s" % 533 | (str(e.errno), e.strerror if e.strerror else str(e.args))) 534 | finally: 535 | if timeout is not None: 536 | sock.settimeout(old_tmout) 537 | 538 | if not received: 539 | raise SessionException( 540 | "Session closed waiting for data: wanted length=%d," 541 | " received length=%d" 542 | % (msgLength, len(msg))) 543 | msg += received 544 | msgLength -= len(received) 545 | 546 | return bytes(msg) 547 | 548 | def stream_recv(self, blocksz=4096, timeout=None): 549 | # type: (int, Optional[float]) -> Generator[bytes, None, None] 550 | """Read data from the socket in blocksz increments. 551 | 552 | Will yield bytes buffers of blocksz for as long as the sender is 553 | sending. After this function completes the socket has been closed. 554 | Note it's best if blocksz is a multiple of 32, to ensure that block 555 | ciphers will work. This code doesn't manage block sizes or padding. 556 | 557 | If timeout is not None, raises a socket.timeout exception on timeout. 558 | The socket is still closed. 559 | """ 560 | sock = self._sock 561 | try: 562 | sock.settimeout(timeout) 563 | while True: 564 | msg = sock.recv(blocksz) 565 | if not msg: 566 | break 567 | if self.__cipherIn: 568 | msg = self.__cipherIn.transform(msg) 569 | yield msg 570 | finally: 571 | self.close() 572 | 573 | def close(self, force=False): 574 | # type: (bool) -> None 575 | """Close the current socket connection with the server.""" 576 | sock = self.__sock 577 | if sock is None: 578 | return 579 | try: 580 | if force: 581 | try: 582 | sock.shutdown(socket.SHUT_RDWR) 583 | except (OSError, socket.error): 584 | # On MacOS this can raise "Socket is not connected" 585 | pass 586 | sock.close() 587 | finally: 588 | self.__sock = None 589 | 590 | def __sendAndReceive(self, message): 591 | # type: (bytes) -> bytes 592 | """Send one message and return the response.""" 593 | self.send(message) 594 | resp = self.recv() 595 | if resp is None: 596 | # This can't actually happen since we have no timeout on recv() 597 | raise RuntimeError("Session.recv() returned None!") 598 | return resp 599 | -------------------------------------------------------------------------------- /pynuodb/statement.py: -------------------------------------------------------------------------------- 1 | """NuoDB Python driver SQL statement. 2 | 3 | (C) Copyright 2013-2023 Dassault Systemes SE. All Rights Reserved. 4 | 5 | This software is licensed under a BSD 3-Clause License. 6 | See the LICENSE file provided with this software. 7 | """ 8 | 9 | try: 10 | from typing import Any, List, Optional # pylint: disable=unused-import 11 | except ImportError: 12 | pass 13 | 14 | 15 | class Statement(object): 16 | """A SQL statement.""" 17 | 18 | def __init__(self, handle): 19 | # type: (int) -> None 20 | """Create a statement. 21 | 22 | :param handle: Handle of the connection. 23 | """ 24 | self.handle = handle 25 | 26 | 27 | class PreparedStatement(Statement): 28 | """A SQL prepared statement.""" 29 | 30 | def __init__(self, handle, parameter_count): 31 | # type: (int, int) -> None 32 | """Create a prepared statement. 33 | 34 | :param handle: Handle of the connection. 35 | :param parameter_count: Number of parameters needed. 36 | """ 37 | super(PreparedStatement, self).__init__(handle) 38 | self.parameter_count = parameter_count 39 | self.description = None # type: Optional[List[List[Any]]] 40 | 41 | 42 | class ExecutionResult(object): 43 | """Result of a statement execution.""" 44 | 45 | def __init__(self, statement, result, row_count): 46 | # type: (Statement, int, int) -> None 47 | """Create the result of a statement execution. 48 | 49 | :param statement: Statement that was executed. 50 | :param result: Result of execution. 51 | :param row_count: Number of rows in the result. 52 | """ 53 | self.result = result 54 | self.row_count = row_count 55 | self.statement = statement 56 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytz>=2015.4 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Set up the NuoDB Python Driver package. 4 | 5 | (C) Copyright 2013-2023 Dassault Systemes SE. All Rights Reserved. 6 | 7 | This software is licensed under a BSD 3-Clause License. 8 | See the LICENSE file provided with this software. 9 | 10 | This package can be installed using pip as follows: 11 | 12 | pip install pynuodb 13 | 14 | To install with cryptography: 15 | 16 | pip install 'pynuodb[crypto]' 17 | 18 | Note cryptography improves performance, but sessions are encrypted even if it 19 | is not intalled. 20 | """ 21 | 22 | import os 23 | import re 24 | 25 | from setuptools import setup 26 | 27 | with open(os.path.join(os.path.dirname(__file__), 'pynuodb', '__init__.py')) as v: 28 | m = re.search(r"^ *__version__ *= *'(.*?)'", v.read(), re.M) 29 | if m is None: 30 | raise RuntimeError("Cannot detect version in pynuodb/__init__.py") 31 | VERSION = m.group(1) 32 | 33 | readme = os.path.join(os.path.dirname(__file__), 'README.rst') 34 | 35 | setup( 36 | name='pynuodb', 37 | version=VERSION, 38 | author='NuoDB', 39 | author_email='drivers@nuodb.com', 40 | description='NuoDB Python driver', 41 | keywords='nuodb scalable cloud database', 42 | packages=['pynuodb'], 43 | url='https://github.com/nuodb/nuodb-python', 44 | license='BSD License', 45 | long_description=open(readme).read(), 46 | install_requires=['pytz>=2015.4', 'ipaddress'], 47 | extras_require=dict(crypto='cryptography>=2.6.1'), 48 | classifiers=[ 49 | 'Development Status :: 5 - Production/Stable', 50 | 'Environment :: Console', 51 | 'Intended Audience :: Developers', 52 | 'License :: OSI Approved :: BSD License', 53 | 'Operating System :: OS Independent', 54 | 'Programming Language :: Python', 55 | 'Programming Language :: Python :: 2', 56 | 'Programming Language :: Python :: 2.7', 57 | 'Programming Language :: Python :: 3', 58 | 'Programming Language :: Python :: 3.6', 59 | 'Programming Language :: SQL', 60 | 'Topic :: Database :: Front-Ends', 61 | ], 62 | ) 63 | -------------------------------------------------------------------------------- /test-performance/timesInsert.py: -------------------------------------------------------------------------------- 1 | # A database named test with user dba / password dba must be created first 2 | 3 | import os 4 | import time 5 | 6 | import pynuodb 7 | 8 | smallIterations = 100 9 | largeIterations = smallIterations * 1000 10 | 11 | 12 | def gettime(): 13 | return time.time() 14 | 15 | 16 | def insert(count): 17 | for i in range(count): 18 | cursor.execute("INSERT INTO perf_test (a,b ) VALUES (%d,'A')" % i) 19 | connection.commit() 20 | 21 | 22 | def select(): 23 | cursor.execute("select * from perf_test") 24 | cursor.fetchall() 25 | 26 | 27 | dropTable = "drop table perf_test cascade if exists" 28 | createTable = "create table perf_test (a int,b char)" 29 | 30 | port = os.environ.get('NUODB_PORT') 31 | if not port: 32 | port = '48004' 33 | 34 | options = {} 35 | trustStore = os.environ.get('NUOCMD_VERIFY_SERVER') 36 | if trustStore: 37 | options = {'trustStore': trustStore, 'verifyHostname': 'False'} 38 | 39 | connection = pynuodb.connect("test", "localhost:" + port, "dba", "dba", 40 | options=options) 41 | cursor = connection.cursor() 42 | cursor.execute("use test") 43 | 44 | # Begin SMALL_INSERT_ITERATIONS test 45 | cursor.execute(dropTable) 46 | cursor.execute(createTable) 47 | start = gettime() 48 | insert(smallIterations) 49 | smallInsertElapsed = gettime() - start 50 | 51 | print("Elapse time of SMALL_INSERT_ITERATIONS = %.4fs" % (smallInsertElapsed)) 52 | 53 | # Begin SMALL_SELECT_ITERATIONS test 54 | start = gettime() 55 | select() 56 | smallSelectElapsed = gettime() - start 57 | print("Elapse time of SMALL_SELECT_ITERATIONS = %.4fs" % (smallSelectElapsed)) 58 | 59 | # Begin LARGE_INSERT_ITERATIONS test 60 | cursor.execute(dropTable) 61 | cursor.execute(createTable) 62 | 63 | start = gettime() 64 | insert(largeIterations) 65 | largeInsertElapsed = gettime() - start 66 | 67 | print("Elapse time of LARGE_INSERT_ITERATIONS = %.4fs" % (largeInsertElapsed)) 68 | 69 | # Begin LARGE_SELECT_ITERATIONS test 70 | start = gettime() 71 | select() 72 | largeSelectElapsed = gettime() - start 73 | 74 | print("Elapse time of LARGE_SELECT_ITERATIONS = %.4fs" % (largeSelectElapsed)) 75 | 76 | if largeInsertElapsed > smallInsertElapsed * 1000: 77 | print("Insert is too slow!") 78 | 79 | if largeSelectElapsed > smallSelectElapsed * 1000: 80 | print("Select is too slow!") 81 | 82 | print("\n") 83 | -------------------------------------------------------------------------------- /test_requirements.txt: -------------------------------------------------------------------------------- 1 | mock>=1.0 2 | nose>=1.3 3 | pytest>=2.7 4 | coverage>=3.7 5 | pytest-cov>=1.8.1 6 | coveralls>=0.5 7 | python-coveralls>=2.5 8 | pynuoadmin 9 | -------------------------------------------------------------------------------- /tests/640px-Starling.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuodb/nuodb-python/cc72a28f58547a81049b2c36ab01f65f2d476b00/tests/640px-Starling.JPG -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | (C) Copyright 2013-2025 Dassault Systemes SE. All Rights Reserved. 3 | 4 | This software is licensed under a BSD 3-Clause License. 5 | See the LICENSE file provided with this software. 6 | """ 7 | 8 | import os 9 | import logging 10 | import copy 11 | import subprocess 12 | import json 13 | 14 | try: 15 | from typing import Any, List, Tuple # pylint: disable=unused-import 16 | except ImportError: 17 | pass 18 | 19 | _log = logging.getLogger("pynuodbtest") 20 | 21 | 22 | def cvtjson(jstr): 23 | # type: (str) -> Any 24 | """Given a string return a valid JSON object. 25 | 26 | Unfortunately the output of nuocmd is not always a valid JSON object; 27 | sometimes it's a dump of one or more JSON objects concatenated together. 28 | As a result this function will always return a JSON list object, even 29 | if the actual output was a single value! 30 | """ 31 | return json.loads(jstr if jstr.startswith('[') else 32 | '[' + jstr.replace('\n}\n{', '\n},\n{') + ']') 33 | 34 | 35 | # Python coverage's subprocess support breaks tests: nuocmd is a Python 2 36 | # script which doesn't have access to the virtenv or whatever pynuodb is 37 | # using. So, nuocmd generates error messages related to coverage then the 38 | # parsing of the JSON output fails. Get rid of the coverage environment 39 | # variables. 40 | env_nocov = copy.copy(os.environ) 41 | env_nocov.pop('COV_CORE_SOURCE', None) 42 | env_nocov.pop('COV_CORE_CONFIG', None) 43 | 44 | 45 | def nuocmd(args, logout=True): 46 | # type: (List[str], bool) -> Tuple[int, str] 47 | _log.info('$ nuocmd ' + ' '.join(args)) 48 | proc = subprocess.Popen(['nuocmd'] + args, env=env_nocov, 49 | stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 50 | (raw, _) = proc.communicate() 51 | ret = proc.wait() 52 | out = raw.decode('UTF-8') 53 | msg = '>exit: %d' % (ret) 54 | if logout or ret != 0: 55 | msg += '\n' + out 56 | _log.info(msg) 57 | return (ret, out) 58 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | (C) Copyright 2025 Dassault Systemes SE. All Rights Reserved. 3 | 4 | This software is licensed under a BSD 3-Clause License. 5 | See the LICENSE file provided with this software. 6 | """ 7 | 8 | import os 9 | import pytest 10 | import random 11 | import string 12 | import tempfile 13 | import time 14 | import socket 15 | import logging 16 | import base64 17 | import shutil 18 | 19 | try: 20 | from typing import Any, Generator, List # pylint: disable=unused-import 21 | from typing import Mapping, NoReturn, Optional, Tuple # pylint: disable=unused-import 22 | 23 | AP_FIXTURE = Tuple[str, str] 24 | ARCHIVE_FIXTURE = Tuple[str, str] 25 | DB_FIXTURE = Tuple[str, str, str] 26 | TE_FIXTURE = str 27 | DATABASE_FIXTURE = Mapping[str, Any] 28 | except ImportError: 29 | pass 30 | 31 | from . import nuocmd, cvtjson 32 | 33 | _log = logging.getLogger("pynuodbtest") 34 | 35 | DB_OPTIONS = [] # type: List[str] 36 | 37 | DATABASE_NAME = 'pynuodb_test' 38 | DBA_USER = 'dba' 39 | DBA_PASSWORD = 'dba_password' 40 | 41 | _CHARS = string.ascii_lowercase + string.digits 42 | 43 | # Unfortunately purging the DB also purges the archive so we have to remember 44 | # this externally from the archive fixture. 45 | __archive_created = False 46 | 47 | 48 | def __fatal(msg): 49 | pytest.exit(msg, returncode=1) 50 | 51 | 52 | def waitforstate(dbname, state, tmout): 53 | # type: (str, str, float) -> None 54 | """Wait TMOUT seconds for database DBNAME to reach STATE.""" 55 | _log.info("Waiting for db %s to reach state %s", dbname, state) 56 | end = time.time() + tmout 57 | while True: 58 | (ret, out) = nuocmd(['--show-json', 'get', 'database', '--db-name', dbname]) 59 | assert ret == 0, "get database failed: %s" % (out) 60 | 61 | now = cvtjson(out)[0].get('state') 62 | if now == state: 63 | _log.info("DB %s is %s", dbname, state) 64 | return 65 | 66 | if time.time() > end: 67 | raise Exception("Timed out waiting for %s" % (state)) 68 | time.sleep(1) 69 | 70 | 71 | @pytest.fixture(scope="session") 72 | def ap(): 73 | # type: () -> Generator[AP_FIXTURE, None, None] 74 | """Find a running AP. It must be started before running tests.""" 75 | _log.info("Retrieving servers") 76 | (ret, out) = nuocmd(['--show-json', 'get', 'servers']) 77 | if ret != 0: 78 | __fatal("Cannot retrieve NuoDB AP servers: %s" % (out)) 79 | 80 | myhost = set(['localhost', socket.getfqdn(), socket.gethostname()]) 81 | for ap in cvtjson(out): 82 | hnm = ap.get('address', '').split(':', 1)[0] 83 | if hnm in myhost or hnm.split('.', 1)[0] in myhost: 84 | localap = ap.get('id') 85 | break 86 | if not localap: 87 | __fatal("No NuoDB AP running on %s" % (str(myhost))) 88 | 89 | # The only way to know the SQL address is via server-config 90 | (ret, out) = nuocmd(['--show-json', 'get', 'server-config', '--server-id', localap]) 91 | if ret != 0: 92 | __fatal("Failed to retrieve config for server %s: %s" % (localap, out)) 93 | cfg = cvtjson(out)[0] 94 | localaddr = '%s:%s' % (cfg['properties']['altAddr'], cfg['properties']['agentPort']) 95 | 96 | # We'll assume that any license at all is enough to run the minimum 97 | # database needed by the tests. 98 | def check_license(): 99 | # type: () -> Optional[str] 100 | (ret, out) = nuocmd(['--show-json', 'get', 'effective-license']) 101 | if ret != 0: 102 | return "Cannot retrieve NuoDB domain license: %s" % (out) 103 | lic = cvtjson(out)[0] 104 | if not lic or 'decodedLicense' not in lic or 'type' not in lic['decodedLicense']: 105 | return "Invalid license: %s" % (out) 106 | if lic['decodedLicense']['type'] == 'UNLICENSED': 107 | return "NuoDB domain is UNLICENSED" 108 | 109 | return None 110 | 111 | _log.info("Checking licensing") 112 | err = check_license() 113 | 114 | # If we need a license and one exists in the environment, install it 115 | if err and os.environ.get('NUODB_LIMITED_LICENSE_CONTENT'): 116 | licfile = 'nuodb%s.lic' % (''.join(random.choice(_CHARS) for x in range(10))) 117 | licpath = os.path.join(tempfile.gettempdir(), licfile) 118 | _log.info("Adding a license provided by the environment") 119 | with open(licpath, 'wb') as f: 120 | f.write(base64.b64decode(os.environ['NUODB_LIMITED_LICENSE_CONTENT'])) 121 | (ret, out) = nuocmd(['set', 'license', '--license-file', licpath]) 122 | try: 123 | os.remove(licpath) 124 | except Exception: 125 | pass 126 | if ret != 0: 127 | __fatal("Failed to set a license: %s" % (out)) 128 | 129 | err = check_license() 130 | 131 | if err: 132 | __fatal(err) 133 | 134 | yield localap, localaddr 135 | 136 | 137 | @pytest.fixture(scope="session") 138 | def archive(request, ap): 139 | # type: (pytest.FixtureRequest, AP_FIXTURE) -> Generator[ARCHIVE_FIXTURE, None, None] 140 | """Find or create an archive. 141 | 142 | :return path, id 143 | """ 144 | localap, _ = ap 145 | global __archive_created 146 | 147 | _log.info("Retriving an archive") 148 | (ret, out) = nuocmd(['--show-json', 'get', 'archives', 149 | '--db-name', DATABASE_NAME]) 150 | ar_id = None 151 | if ret == 0: 152 | ars = cvtjson(out) 153 | if len(ars): 154 | ar_id = ars[0]['id'] 155 | ar_path = ars[0]['path'] 156 | _log.info("Using existing archive %s: %s", ar_id, ar_path) 157 | 158 | if not ar_id: 159 | ardir = DATABASE_NAME + '-' + ''.join(random.choice(_CHARS) for x in range(20)) 160 | ar_path = os.path.join(tempfile.gettempdir(), ardir) 161 | _log.info("Creating archive %s", ar_path) 162 | (ret, out) = nuocmd(['--show-json', 'create', 'archive', 163 | '--db-name', DATABASE_NAME, 164 | '--server-id', localap, 165 | '--archive-path', ar_path]) 166 | if ret != 0: 167 | __fatal("Unable to create archive %s: %s" % (ar_path, out)) 168 | ar = cvtjson(out)[0] 169 | ar_id = ar.get('id') 170 | __archive_created = True 171 | 172 | yield ar_path, ar_id 173 | 174 | if __archive_created: 175 | (ret, out) = nuocmd(['delete', 'archive', '--archive-id', str(ar_id)]) 176 | assert ret == 0, "Failed to delete archive %s: %s" % (ar_id, out) 177 | 178 | # If nothing failed then delete the archive, else leave it for forensics 179 | if request.session.testsfailed == 0: 180 | shutil.rmtree(ar_path, ignore_errors=True) 181 | 182 | 183 | @pytest.fixture(scope="session") 184 | def get_db(archive): 185 | # type: (ARCHIVE_FIXTURE) -> Generator[DB_FIXTURE, None, None] 186 | _log.info("Retrieving database %s", DATABASE_NAME) 187 | (ret, out) = nuocmd(['--show-json', 'get', 'database', 188 | '--db-name', DATABASE_NAME]) 189 | created = True 190 | if ret == 0: 191 | db = cvtjson(out) 192 | if db and db[0].get('state') != 'TOMBSTONE': 193 | _log.info("Using existing database %s", DATABASE_NAME) 194 | (ret, out) = nuocmd(['update', 'database-options', 195 | '--db-name', DATABASE_NAME, 196 | '--default-options'] + DB_OPTIONS) 197 | if ret != 0: 198 | __fatal("Failed to reset database options: %s" % (out)) 199 | # We assume that the correct username / password are configured! 200 | # This is to support fast test cycles: pre-creating the database 201 | # avoids the overhead of creating it for each test run. 202 | created = False 203 | 204 | if created: 205 | _log.info("Creating database %s", DATABASE_NAME) 206 | (ret, out) = nuocmd(['create', 'database', '--db-name', DATABASE_NAME, 207 | '--no-autostart', 208 | '--dba-user', DBA_USER, 209 | '--dba-password', DBA_PASSWORD, 210 | '--default-options'] + DB_OPTIONS) 211 | if ret != 0: 212 | __fatal("Failed to create database %s: %s" % (DATABASE_NAME, out)) 213 | 214 | yield DATABASE_NAME, DBA_USER, DBA_PASSWORD 215 | 216 | if created: 217 | _log.info("Deleting database %s", DATABASE_NAME) 218 | (ret, out) = nuocmd(['delete', 'database', '--purge', '--db-name', DATABASE_NAME]) 219 | assert ret == 0, "Failed to delete %s: %s" % (DATABASE_NAME, out) 220 | global __archive_created 221 | __archive_created = False 222 | 223 | 224 | @pytest.fixture(scope="session") 225 | def db(get_db): 226 | # type: (DB_FIXTURE) -> Generator[DB_FIXTURE, None, None] 227 | dbname = get_db[0] 228 | 229 | was_running = False 230 | 231 | (ret, out) = nuocmd(['--show-json', 'get', 'database', '--db-name', dbname]) 232 | if ret != 0: 233 | __fatal("Failed to get db state %s: %s" % (dbname, out)) 234 | state = cvtjson(out)[0]['state'] 235 | if state == 'TOMBSTONE': 236 | __fatal("Database %s has exited: %s" % (dbname, out)) 237 | if state == 'RUNNING': 238 | was_running = True 239 | _log.info("Database %s is already running", dbname) 240 | else: 241 | (ret, out) = nuocmd(['start', 'database', '--db-name', dbname]) 242 | if ret != 0: 243 | __fatal("Failed to start database: %s" % (out)) 244 | waitforstate(dbname, 'RUNNING', 30) 245 | _log.info("Started database %s", dbname) 246 | 247 | yield get_db[0], get_db[1], get_db[2] 248 | 249 | if not was_running: 250 | (ret, out) = nuocmd(['shutdown', 'database', '--db-name', dbname]) 251 | assert ret == 0, "Failed to stop database %s: %s" % (dbname, out) 252 | waitforstate(dbname, 'NOT_RUNNING', 30) 253 | 254 | 255 | @pytest.fixture(scope="session") 256 | def te(ap, db): 257 | # type: (AP_FIXTURE, DB_FIXTURE) -> Generator[TE_FIXTURE, None, None] 258 | localap, _ = ap 259 | dbname = db[0] 260 | 261 | start_id = None 262 | started = False 263 | 264 | (ret, out) = nuocmd(['--show-json', 'get', 'processes', '--db-name', dbname]) 265 | if ret != 0: 266 | __fatal("Failed to get db processes %s: %s" % (dbname, out)) 267 | for proc in cvtjson(out): 268 | if proc['state'] == 'RUNNING' and proc['options']['engine-type'] == 'TE': 269 | start_id = proc['startId'] 270 | _log.info("Using existing TE with sid:%s", start_id) 271 | break 272 | else: 273 | (ret, out) = nuocmd(['--show-json', 'start', 'process', '--db-name', dbname, 274 | '--engine-type', 'TE', '--server-id', localap]) 275 | if ret != 0: 276 | __fatal("Failed to start TE: %s" % (out)) 277 | start_id = cvtjson(out)[0]['startId'] 278 | started = True 279 | _log.info("Created a TE with sid:%s", start_id) 280 | 281 | yield start_id 282 | 283 | if started: 284 | (ret, out) = nuocmd(['shutdown', 'process', '--start-id', start_id]) 285 | assert ret == 0, "Failed to stop TE %s: %s" % (start_id, out) 286 | 287 | 288 | @pytest.fixture(scope='session') 289 | def database(ap, db, te): 290 | # type: (AP_FIXTURE, DB_FIXTURE, TE_FIXTURE) -> DATABASE_FIXTURE 291 | import pynuodb 292 | end = time.time() + 30 293 | conn = None 294 | _log.info("Creating a SQL connection to %s as user %s with schema 'test'", 295 | db[0], db[1]) 296 | 297 | connect_args = {'database': db[0], 298 | 'host': ap[1], 299 | 'user': db[1], 300 | 'password': db[2], 301 | 'options': {'schema': 'test'}} # type: DATABASE_FIXTURE 302 | try: 303 | while True: 304 | try: 305 | conn = pynuodb.connect(**connect_args) 306 | break 307 | except pynuodb.session.SessionException: 308 | pass 309 | if time.time() > end: 310 | raise Exception("Timed out waiting for a TE to be ready") 311 | time.sleep(1) 312 | finally: 313 | if conn: 314 | conn.close() 315 | 316 | _log.info("Database %s is available", db[0]) 317 | 318 | return connect_args 319 | -------------------------------------------------------------------------------- /tests/dbapi20_tpc.py: -------------------------------------------------------------------------------- 1 | """ Python DB API 2.0 driver Two Phase Commit compliance test suite. 2 | 3 | """ 4 | 5 | import unittest 6 | 7 | 8 | class TwoPhaseCommitTests(unittest.TestCase): 9 | 10 | driver = None 11 | 12 | def connect(self): 13 | """Make a database connection.""" 14 | raise NotImplementedError 15 | 16 | _last_id = 0 17 | _global_id_prefix = "dbapi20_tpc:" 18 | 19 | def make_xid(self, con): 20 | id = TwoPhaseCommitTests._last_id 21 | TwoPhaseCommitTests._last_id += 1 22 | return con.xid(42, "%s%d" % (self._global_id_prefix, id), "qualifier") 23 | 24 | def test_xid(self): 25 | con = self.connect() 26 | try: 27 | xid = con.xid(42, "global", "bqual") 28 | except self.driver.NotSupportedError: 29 | self.fail("Driver does not support transaction IDs.") 30 | 31 | self.assertEqual(xid[0], 42) 32 | self.assertEqual(xid[1], "global") 33 | self.assertEqual(xid[2], "bqual") 34 | 35 | # Try some extremes for the transaction ID: 36 | xid = con.xid(0, "", "") 37 | self.assertEqual(tuple(xid), (0, "", "")) 38 | xid = con.xid(0x7fffffff, "a" * 64, "b" * 64) 39 | self.assertEqual(tuple(xid), (0x7fffffff, "a" * 64, "b" * 64)) 40 | 41 | def test_tpc_begin(self): 42 | con = self.connect() 43 | try: 44 | xid = self.make_xid(con) 45 | try: 46 | con.tpc_begin(xid) 47 | except self.driver.NotSupportedError: 48 | self.fail("Driver does not support tpc_begin()") 49 | finally: 50 | con.close() 51 | 52 | def test_tpc_commit_without_prepare(self): 53 | con = self.connect() 54 | try: 55 | xid = self.make_xid(con) 56 | con.tpc_begin(xid) 57 | cursor = con.cursor() 58 | cursor.execute("SELECT 1") 59 | con.tpc_commit() 60 | finally: 61 | con.close() 62 | 63 | def test_tpc_rollback_without_prepare(self): 64 | con = self.connect() 65 | try: 66 | xid = self.make_xid(con) 67 | con.tpc_begin(xid) 68 | cursor = con.cursor() 69 | cursor.execute("SELECT 1") 70 | con.tpc_rollback() 71 | finally: 72 | con.close() 73 | 74 | def test_tpc_commit_with_prepare(self): 75 | con = self.connect() 76 | try: 77 | xid = self.make_xid(con) 78 | con.tpc_begin(xid) 79 | cursor = con.cursor() 80 | cursor.execute("SELECT 1") 81 | con.tpc_prepare() 82 | con.tpc_commit() 83 | finally: 84 | con.close() 85 | 86 | def test_tpc_rollback_with_prepare(self): 87 | con = self.connect() 88 | try: 89 | xid = self.make_xid(con) 90 | con.tpc_begin(xid) 91 | cursor = con.cursor() 92 | cursor.execute("SELECT 1") 93 | con.tpc_prepare() 94 | con.tpc_rollback() 95 | finally: 96 | con.close() 97 | 98 | def test_tpc_begin_in_transaction_fails(self): 99 | con = self.connect() 100 | try: 101 | xid = self.make_xid(con) 102 | 103 | cursor = con.cursor() 104 | cursor.execute("SELECT 1") 105 | self.assertRaises(self.driver.ProgrammingError, 106 | con.tpc_begin, xid) 107 | finally: 108 | con.close() 109 | 110 | def test_tpc_begin_in_tpc_transaction_fails(self): 111 | con = self.connect() 112 | try: 113 | xid = self.make_xid(con) 114 | 115 | cursor = con.cursor() 116 | cursor.execute("SELECT 1") 117 | self.assertRaises(self.driver.ProgrammingError, 118 | con.tpc_begin, xid) 119 | finally: 120 | con.close() 121 | 122 | def test_commit_in_tpc_fails(self): 123 | # calling commit() within a TPC transaction fails with 124 | # ProgrammingError. 125 | con = self.connect() 126 | try: 127 | xid = self.make_xid(con) 128 | con.tpc_begin(xid) 129 | 130 | self.assertRaises(self.driver.ProgrammingError, con.commit) 131 | finally: 132 | con.close() 133 | 134 | def test_rollback_in_tpc_fails(self): 135 | # calling rollback() within a TPC transaction fails with 136 | # ProgrammingError. 137 | con = self.connect() 138 | try: 139 | xid = self.make_xid(con) 140 | con.tpc_begin(xid) 141 | 142 | self.assertRaises(self.driver.ProgrammingError, con.rollback) 143 | finally: 144 | con.close() 145 | -------------------------------------------------------------------------------- /tests/mock_tzs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from datetime import tzinfo 5 | from datetime import datetime 6 | import os 7 | 8 | import pytz 9 | 10 | import pynuodb 11 | 12 | if os.path.exists('/etc/timezone'): 13 | with open('/etc/timezone') as tzf: 14 | Local = pytz.timezone(tzf.read().strip()) 15 | else: 16 | with open('/etc/localtime', 'rb') as tlf: 17 | Local = pytz.build_tzinfo('localtime', tlf) # type: ignore 18 | 19 | UTC = pytz.timezone('UTC') 20 | 21 | 22 | class _MyOffset(tzinfo): 23 | ''' 24 | A timezone class that uses the current offset for all times in the past and 25 | future. The database doesn't return an timezone offset to us, it just 26 | returns the timestamp it has, but cast into the client's current timezone. 27 | This class can be used to do exactly the same thing to the test val. 28 | ''' 29 | def utcoffset(self, dt): 30 | return Local.localize(datetime.now()).utcoffset() 31 | 32 | 33 | MyOffset = _MyOffset() 34 | 35 | 36 | class EscapingTimestamp(pynuodb.Timestamp): 37 | ''' 38 | An EscapingTimestamp is just like a regular pynuodb.Timestamp, except that 39 | it's string representation is a bit of executable SQL that constructs the 40 | correct timestamp on the server side. This is necessary until [DB-2251] is 41 | fixed and we can interpret straight strings of the kind that 42 | pynuodb.Timestamp produces. 43 | ''' 44 | py2sql = { 45 | '%Y': 'YYYY', 46 | '%m': 'MM', 47 | '%d': 'dd', 48 | '%H': 'HH', 49 | '%M': 'mm', 50 | '%S': 'ss', 51 | '%f000': 'SSSSSSSSS', 52 | '%z': 'ZZZZ'} 53 | 54 | def __str__(self): 55 | pyformat = '%Y-%m-%d %H:%M:%S.%f000 %z' 56 | sqlformat = pyformat 57 | for pyspec, sqlspec in self.py2sql.items(): 58 | sqlformat = sqlformat.replace(pyspec, sqlspec) 59 | return "DATE_FROM_STR('%s', '%s')" % (self.strftime(pyformat), sqlformat) 60 | 61 | 62 | if __name__ == '__main__': 63 | print(str(EscapingTimestamp(2014, 7, 15, 23, 59, 58, 72, Local))) 64 | print(repr(EscapingTimestamp(2014, 7, 15, 23, 59, 58, 72, Local))) 65 | print(str(EscapingTimestamp(2014, 12, 15, 23, 59, 58, 72, Local))) 66 | -------------------------------------------------------------------------------- /tests/nuodb_base.py: -------------------------------------------------------------------------------- 1 | """ 2 | (C) Copyright 2013-2025 Dassault Systemes SE. All Rights Reserved. 3 | 4 | This software is licensed under a BSD 3-Clause License. 5 | See the LICENSE file provided with this software. 6 | """ 7 | 8 | import pytest 9 | import copy 10 | 11 | try: 12 | from typing import Any # pylint: disable=unused-import 13 | except ImportError: 14 | pass 15 | 16 | import pynuodb 17 | 18 | from . import nuocmd, cvtjson 19 | 20 | 21 | class NuoBase(object): 22 | longMessage = True 23 | 24 | # Set the driver module for the imported test suites 25 | driver = pynuodb # type: Any 26 | 27 | connect_args = () 28 | host = None 29 | 30 | lower_func = 'lower' # For stored procedure test 31 | 32 | @pytest.fixture(autouse=True) 33 | def _setup(self, database): 34 | # Preserve the options we'll need to create a connection to the DB 35 | self.connect_args = database 36 | 37 | # Verify the database is up and has a running TE 38 | dbname = database['database'] 39 | (ret, out) = nuocmd(['--show-json', 'get', 'processes', 40 | '--db-name', dbname], logout=False) 41 | assert ret == 0, "DB not running: %s" % (out) 42 | 43 | for proc in cvtjson(out): 44 | if proc.get('type') == 'TE' and proc.get('state') == 'RUNNING': 45 | break 46 | else: 47 | (ret, out) = nuocmd(['show', 'domain']) 48 | assert ret == 0, "Failed to show domain: %s" % (out) 49 | pytest.fail("No running TEs found:\n%s" % (out)) 50 | 51 | def _connect(self, options=None): 52 | connect_args = copy.deepcopy(self.connect_args) 53 | if options: 54 | if 'options' not in connect_args: 55 | connect_args['options'] = {} 56 | for k, v in options.items(): 57 | if v is not None: 58 | connect_args['options'][k] = v 59 | elif k in connect_args['options']: 60 | del connect_args['options'][k] 61 | if not connect_args['options']: 62 | del connect_args['options'] 63 | return pynuodb.connect(**connect_args) 64 | -------------------------------------------------------------------------------- /tests/nuodb_blob_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | (C) Copyright 2013-2025 Dassault Systemes SE. All Rights Reserved. 3 | 4 | This software is licensed under a BSD 3-Clause License. 5 | See the LICENSE file provided with this software. 6 | """ 7 | 8 | import struct 9 | 10 | import pynuodb 11 | import sys 12 | from . import nuodb_base 13 | 14 | systemVersion = sys.version[0] 15 | 16 | 17 | class TestNuoDBBlob(nuodb_base.NuoBase): 18 | def test_blob_prepared(self): 19 | con = self._connect() 20 | cursor = con.cursor() 21 | 22 | binary_data = struct.pack('hhl', 1, 2, 3) 23 | 24 | cursor.execute("SELECT ? FROM DUAL", [pynuodb.Binary(binary_data)]) 25 | row = cursor.fetchone() 26 | 27 | currentRow = str(row[0]) 28 | if systemVersion == '3': 29 | currentRow = bytes(currentRow, 'latin-1') 30 | array2 = struct.unpack('hhl', currentRow) 31 | assert len(array2) == 3 32 | assert array2[2] == 3 33 | -------------------------------------------------------------------------------- /tests/nuodb_connect_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | (C) Copyright 2013-2025 Dassault Systemes SE. All Rights Reserved. 3 | 4 | This software is licensed under a BSD 3-Clause License. 5 | See the LICENSE file provided with this software. 6 | """ 7 | 8 | import socket 9 | import pytest 10 | 11 | import pynuodb 12 | from pynuodb.exception import ProgrammingError 13 | from pynuodb.session import SessionException 14 | 15 | from . import nuodb_base 16 | import pynuodb.crypt 17 | 18 | 19 | class TestNuoDBConnect(nuodb_base.NuoBase): 20 | def test_nosuchdatabase(self): 21 | with pytest.raises(SessionException): 22 | pynuodb.connect("nosuchdatabase", 23 | self.connect_args['host'], 24 | self.connect_args['user'], 25 | self.connect_args['password']) 26 | 27 | def test_nosuchport(self): 28 | # Different versions of Python give different exceptions here 29 | try: 30 | pynuodb.connect(self.connect_args['database'], "localhost:23456", 31 | self.connect_args['user'], 32 | self.connect_args['password']) 33 | pytest.fail("Connection to bogus port succeeded") 34 | except Exception: 35 | pass 36 | 37 | def test_nosuchhost(self): 38 | with pytest.raises(socket.gaierror): 39 | pynuodb.connect(self.connect_args['database'], "nosuchhost", 40 | self.connect_args['user'], 41 | self.connect_args['password']) 42 | 43 | def test_nosuchuser(self): 44 | with pytest.raises(ProgrammingError): 45 | pynuodb.connect(self.connect_args['database'], 46 | self.connect_args['host'], 47 | "nosuchuser", 48 | self.connect_args['password']) 49 | 50 | def test_nosuchpassword(self): 51 | with pytest.raises(ProgrammingError): 52 | pynuodb.connect(self.connect_args['database'], 53 | self.connect_args['host'], 54 | self.connect_args['user'], 55 | "nosuchpassword") 56 | 57 | @pytest.mark.skipif(not pynuodb.crypt.AESImported, reason="No AES available") 58 | def test_aes256cipher(self): 59 | conn = self._connect(options={'ciphers': 'AES-256-CTR'}) 60 | try: 61 | config = conn.connection_config() 62 | assert config['cipher'] == 'AES-256' 63 | conn.testConnection() 64 | finally: 65 | conn.close() 66 | 67 | @pytest.mark.skipif(not pynuodb.crypt.AESImported, reason="No AES available") 68 | def test_aes128cipher(self): 69 | conn = self._connect(options={'ciphers': 'AES-128-CTR'}) 70 | try: 71 | config = conn.connection_config() 72 | assert config['cipher'] == 'AES-128' 73 | conn.testConnection() 74 | finally: 75 | conn.close() 76 | 77 | def test_rc4cipher(self): 78 | conn = self._connect(options={'ciphers': 'RC4'}) 79 | try: 80 | config = conn.connection_config() 81 | assert config['cipher'].startswith('RC4') 82 | conn.testConnection() 83 | finally: 84 | conn.close() 85 | -------------------------------------------------------------------------------- /tests/nuodb_crypt_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | (C) Copyright 2013-2025 Dassault Systemes SE. All Rights Reserved. 3 | 4 | This software is licensed under a BSD 3-Clause License. 5 | See the LICENSE file provided with this software. 6 | """ 7 | 8 | import pynuodb 9 | 10 | 11 | class TestNuoDBBasic(object): 12 | """Run basic tests of the crypt module.""" 13 | 14 | CVT = {1: bytearray([1]), 15 | 127: bytearray([127]), 16 | 254: bytearray([0, 254]), 17 | 255: bytearray([0, 255]), 18 | -1: bytearray([255]), 19 | -2: bytearray([254]), 20 | -256: bytearray([255, 0]), 21 | -258: bytearray([254, 254])} 22 | 23 | def test_toByteString(self): 24 | """Test toSignedByteString.""" 25 | for val, data in self.CVT.items(): 26 | assert pynuodb.crypt.toSignedByteString(val) == data 27 | 28 | def test_fromByteString(self): 29 | """Test fromSignedByteString.""" 30 | for val, data in self.CVT.items(): 31 | assert pynuodb.crypt.fromSignedByteString(data) == val 32 | 33 | def test_bothByteString(self): 34 | """Test to and from signed bytes tring.""" 35 | for val in self.CVT.keys(): 36 | assert pynuodb.crypt.fromSignedByteString(pynuodb.crypt.toSignedByteString(val)) == val 37 | -------------------------------------------------------------------------------- /tests/nuodb_cursor_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | (C) Copyright 2013-2025 Dassault Systemes SE. All Rights Reserved. 3 | 4 | This software is licensed under a BSD 3-Clause License. 5 | See the LICENSE file provided with this software. 6 | """ 7 | 8 | import pytest 9 | 10 | from pynuodb.exception import DataError, ProgrammingError, BatchError, OperationalError 11 | 12 | from . import nuodb_base 13 | 14 | 15 | class TestNuoDBCursor(nuodb_base.NuoBase): 16 | 17 | def test_cursor_description(self): 18 | con = self._connect() 19 | cursor = con.cursor() 20 | 21 | cursor.execute("SELECT 'abc' AS XYZ, 123 AS `123` FROM DUAL") 22 | descriptions = cursor.description 23 | dstr = "Descriptions: %s" % (str(descriptions)) 24 | assert len(descriptions) == 2, dstr 25 | 26 | assert descriptions[0][0] == 'XYZ', dstr 27 | assert descriptions[0][1] == self.driver.STRING, dstr 28 | # We don't get back a length for this type (it's 0) 29 | # assert descriptions[0][2] == 3, dstr 30 | 31 | assert descriptions[1][0] == '123', dstr 32 | assert descriptions[1][1] == self.driver.NUMBER, dstr 33 | # I think this should be 6 but there is disagreement? 34 | # assert descriptions[1][2] == 5, dstr 35 | 36 | def test_cursor_rowcount_and_last_query(self): 37 | con = self._connect() 38 | cursor = con.cursor() 39 | statement = "SELECT 1 FROM DUAL UNION ALL SELECT 2 FROM DUAL" 40 | cursor.execute(statement) 41 | assert cursor.rowcount == -1 42 | assert cursor.query == statement 43 | 44 | def test_insufficient_parameters(self): 45 | con = self._connect() 46 | cursor = con.cursor() 47 | 48 | with pytest.raises(ProgrammingError): 49 | cursor.execute("SELECT ?, ? FROM DUAL", [1]) 50 | 51 | def test_toomany_parameters(self): 52 | con = self._connect() 53 | cursor = con.cursor() 54 | 55 | with pytest.raises(ProgrammingError): 56 | cursor.execute("SELECT 1 FROM DUAL", [1]) 57 | 58 | with pytest.raises(ProgrammingError): 59 | cursor.execute("SELECT ? FROM DUAL", [1, 2]) 60 | 61 | def test_incorrect_parameters(self): 62 | con = self._connect() 63 | cursor = con.cursor() 64 | 65 | with pytest.raises(DataError): 66 | cursor.execute("SELECT ? + 1 FROM DUAL", ['abc']) 67 | 68 | def test_executemany(self): 69 | con = self._connect() 70 | cursor = con.cursor() 71 | 72 | cursor.execute("DROP TABLE IF EXISTS executemany_table") 73 | cursor.execute("CREATE TABLE executemany_table (f1 INTEGER, f2 INTEGER)") 74 | cursor.executemany("INSERT INTO executemany_table VALUES (?, ?)", [[1, 2], [3, 4]]) 75 | 76 | cursor.execute("SELECT * FROM executemany_table") 77 | 78 | ret = cursor.fetchall() 79 | 80 | assert ret[0][0] == 1 81 | assert ret[0][1] == 2 82 | assert ret[1][0] == 3 83 | assert ret[1][1] == 4 84 | 85 | cursor.execute("DROP TABLE executemany_table") 86 | 87 | def test_executemany_bad_parameters(self): 88 | con = self._connect() 89 | cursor = con.cursor() 90 | cursor.execute("DROP TABLE IF EXISTS executemany_table") 91 | cursor.execute("CREATE TABLE executemany_table (f1 INTEGER, f2 INTEGER)") 92 | # 3rd tuple has too many params 93 | with pytest.raises(ProgrammingError): 94 | cursor.executemany("INSERT INTO executemany_table VALUES (?, ?)", [[1, 2], [3, 4], [1, 2, 3]]) 95 | 96 | cursor.execute("DROP TABLE executemany_table") 97 | 98 | def test_executemany_somefail(self): 99 | con = self._connect() 100 | cursor = con.cursor() 101 | cursor.execute("DROP TABLE IF EXISTS executemany_table") 102 | cursor.execute("CREATE TABLE executemany_table (f1 INTEGER, f2 INTEGER)") 103 | cursor.execute('CREATE UNIQUE INDEX "f1idx" ON "executemany_table" ("f1");') 104 | # 3rd tuple has uniqueness conflict 105 | with pytest.raises(BatchError) as ex: 106 | cursor.executemany("INSERT INTO executemany_table VALUES (?, ?)", 107 | [[1, 2], [3, 4], [1, 2], [5, 6], [5, 6]]) 108 | 109 | assert ex.value.results[0] == 1 110 | assert ex.value.results[1] == 1 111 | assert ex.value.results[2] == -3 112 | assert ex.value.results[3] == 1 113 | assert ex.value.results[4] == -3 114 | 115 | # test that they all made it save the bogus one 116 | cursor.execute("select * from executemany_table;") 117 | assert len(cursor.fetchall()) == 3 118 | 119 | cursor.execute("DROP TABLE executemany_table") 120 | 121 | def test_result_set_gets_closed(self): 122 | # Server will throw error after 1000 open result sets 123 | con = self._connect() 124 | for j in [False, True]: 125 | for i in range(2015): 126 | if not j: 127 | cursor = con.cursor() 128 | cursor.execute('select 1 from dual;') 129 | con.commit() 130 | cursor.close() 131 | else: 132 | if i >= 1000: 133 | with pytest.raises(OperationalError): 134 | cursor = con.cursor() 135 | cursor.execute('select 1 from dual;') 136 | con.commit() 137 | else: 138 | cursor = con.cursor() 139 | cursor.execute('select 1 from dual;') 140 | con.commit() 141 | -------------------------------------------------------------------------------- /tests/nuodb_dbapi20_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | (C) Copyright 2013-2025 Dassault Systemes SE. All Rights Reserved. 3 | 4 | This software is licensed under a BSD 3-Clause License. 5 | See the LICENSE file provided with this software. 6 | """ 7 | 8 | # Since we use pytest, not unittest, the tests in dbapi20 are ignored: 9 | # that file doesn't end in "...test.py" and the class defined there doesn't 10 | # start with "Test..." 11 | # Instead we wrap those tests in a pytest test class in this file. 12 | from . import dbapi20 13 | from . import nuodb_base 14 | 15 | 16 | class TestNuoDBAPI20(nuodb_base.NuoBase, dbapi20.DatabaseAPI20Test): 17 | # Unsupported tests 18 | def test_nextset(self): 19 | pass 20 | 21 | def test_setoutputsize(self): 22 | pass 23 | 24 | def test_callproc(self): 25 | pass 26 | -------------------------------------------------------------------------------- /tests/nuodb_description_name_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | (C) Copyright 2025 Dassault Systemes SE. All Rights Reserved. 3 | 4 | This software is licensed under a BSD 3-Clause License. 5 | See the LICENSE file provided with this software. 6 | """ 7 | 8 | import decimal 9 | import datetime 10 | 11 | from . import nuodb_base 12 | 13 | 14 | class TestNuoDBDescription(nuodb_base.NuoBase): 15 | 16 | def test_description(self): 17 | con = self._connect() 18 | cursor = con.cursor() 19 | 20 | cursor.execute("CREATE TEMPORARY TABLE tmp (v1 INTEGER, v2 STRING)" ) 21 | cursor.execute("INSERT INTO tmp VALUES (1,'a'), (2,'b')") 22 | cursor.execute("SELECT v1 AS c1, concat(v2,'') as c2 FROM tmp") 23 | row = cursor.fetchone() 24 | d = cursor.description 25 | 26 | assert d[0][0].lower() == 'c1' 27 | assert d[1][0].lower() == 'c2' 28 | -------------------------------------------------------------------------------- /tests/nuodb_executionflow_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | (C) Copyright 2013-2025 Dassault Systemes SE. All Rights Reserved. 3 | 4 | This software is licensed under a BSD 3-Clause License. 5 | See the LICENSE file provided with this software. 6 | 7 | This tests checks for various out-of-order execution situations. 8 | E.g., attempting to run a query after being disconnected from the database. 9 | """ 10 | 11 | import pytest 12 | 13 | from pynuodb.exception import Error 14 | 15 | from . import nuodb_base 16 | 17 | 18 | class TestNuoDBExecutionFlow(nuodb_base.NuoBase): 19 | def test_commit_after_disconnect(self): 20 | con = self._connect() 21 | 22 | con.close() 23 | 24 | with pytest.raises(Error) as ex: 25 | con.commit() 26 | assert str(ex.value) == 'connection is closed' 27 | 28 | def test_cursor_after_disconnect(self): 29 | con = self._connect() 30 | 31 | con.close() 32 | 33 | with pytest.raises(Error) as ex: 34 | con.cursor() 35 | assert str(ex.value) == 'connection is closed' 36 | 37 | def test_execute_after_disconnect(self): 38 | con = self._connect() 39 | 40 | cursor = con.cursor() 41 | con.close() 42 | 43 | with pytest.raises(Error) as ex: 44 | cursor.execute("SELECT 1 FROM DUAL") 45 | assert str(ex.value) == 'connection is closed' 46 | 47 | def test_fetchone_after_disconnect(self): 48 | con = self._connect() 49 | 50 | cursor = con.cursor() 51 | cursor.execute("SELECT 1 FROM DUAL") 52 | con.close() 53 | 54 | with pytest.raises(Error) as ex: 55 | cursor.fetchone() 56 | assert str(ex.value) == 'connection is closed' 57 | 58 | def test_execute_after_close(self): 59 | con = self._connect() 60 | cursor = con.cursor() 61 | 62 | cursor.close() 63 | 64 | with pytest.raises(Error) as ex: 65 | cursor.execute("SELECT 1 FROM DUAL") 66 | assert str(ex.value) == 'cursor is closed' 67 | 68 | def test_fetchone_without_execute(self): 69 | con = self._connect() 70 | cursor = con.cursor() 71 | 72 | with pytest.raises(Error) as ex: 73 | cursor.fetchone() 74 | assert str(ex.value) == 'Previous execute did not produce any results or no call was issued yet' 75 | 76 | def test_fetchone_after_close(self): 77 | con = self._connect() 78 | cursor = con.cursor() 79 | cursor.execute("SELECT 1 FROM DUAL") 80 | cursor.close() 81 | 82 | with pytest.raises(Error) as ex: 83 | cursor.fetchone() 84 | assert str(ex.value) == 'cursor is closed' 85 | 86 | def test_fetchone_on_ddl(self): 87 | con = self._connect() 88 | cursor = con.cursor() 89 | cursor.execute("DROP TABLE fetchone_on_ddl IF EXISTS") 90 | 91 | with pytest.raises(Error) as ex: 92 | cursor.fetchone() 93 | assert str(ex.value) == 'Previous execute did not produce any results or no call was issued yet' 94 | 95 | def test_fetchone_on_empty(self): 96 | con = self._connect() 97 | cursor = con.cursor() 98 | cursor.execute("SELECT 1 FROM DUAL WHERE FALSE") 99 | assert cursor.fetchone() is None 100 | 101 | def test_fetchone_beyond_eof(self): 102 | con = self._connect() 103 | cursor = con.cursor() 104 | 105 | cursor.execute("SELECT 1 FROM DUAL") 106 | cursor.fetchone() 107 | assert cursor.fetchone() is None 108 | 109 | def test_fetchmany_beyond_eof(self): 110 | con = self._connect() 111 | cursor = con.cursor() 112 | 113 | cursor.execute("SELECT 1 FROM DUAL UNION ALL SELECT 2 FROM DUAL") 114 | many = cursor.fetchmany(100) 115 | assert len(many) == 2 116 | 117 | def test_fetch_after_error(self): 118 | con = self._connect() 119 | cursor = con.cursor() 120 | 121 | with pytest.raises(Error) as e1: 122 | cursor.execute("SYNTAX ERROR") 123 | assert str(e1.value) == 'SYNTAX_ERROR: syntax error on line 1\nSYNTAX ERROR\n^ expected statement got SYNTAX\n' 124 | 125 | with pytest.raises(Error) as e2: 126 | cursor.fetchone() 127 | assert str(e2.value) == 'Previous execute did not produce any results or no call was issued yet' 128 | 129 | def test_execute_after_error(self): 130 | con = self._connect() 131 | cursor = con.cursor() 132 | 133 | with pytest.raises(Error) as ex: 134 | cursor.execute("syntax error") 135 | assert str(ex.value) == 'SYNTAX_ERROR: syntax error on line 1\nsyntax error\n^ expected statement got syntax\n' 136 | 137 | cursor.execute("SELECT 1 FROM DUAL") 138 | cursor.fetchone() 139 | 140 | def test_error_after_error(self): 141 | con = self._connect() 142 | cursor = con.cursor() 143 | 144 | with pytest.raises(Error) as e1: 145 | cursor.execute("syntax1 error") 146 | assert str(e1.value) == 'SYNTAX_ERROR: syntax error on line 1\nsyntax1 error\n^ expected statement got syntax1\n' 147 | 148 | with pytest.raises(Error) as e2: 149 | cursor.execute("syntax2 error") 150 | assert str(e2.value) == 'SYNTAX_ERROR: syntax error on line 1\nsyntax2 error\n^ expected statement got syntax2\n' 151 | 152 | def test_execute_ten_million_with_result_sets(self): 153 | con = self._connect() 154 | cursor = con.cursor() 155 | cursor.execute("DROP TABLE IF EXISTS execute_ten_million_with_result_sets") 156 | cursor.execute("CREATE TABLE execute_ten_million_with_result_sets (value INTEGER)") 157 | for i in range(10000): 158 | cursor.execute("insert into execute_ten_million_with_result_sets (value) Values (%d)" % (i)) 159 | con.commit() 160 | cursor.execute("select count(*) from execute_ten_million_with_result_sets;") 161 | res = cursor.fetchone()[0] 162 | assert res == i + 1 163 | -------------------------------------------------------------------------------- /tests/nuodb_globals_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | (C) Copyright 2013-2025 Dassault Systemes SE. All Rights Reserved. 3 | 4 | This software is licensed under a BSD 3-Clause License. 5 | See the LICENSE file provided with this software. 6 | """ 7 | 8 | import pynuodb 9 | 10 | from . import nuodb_base 11 | 12 | 13 | class TestNuoDBGlobals(nuodb_base.NuoBase): 14 | def test_module_globals(self): 15 | assert pynuodb.apilevel == '2.0' 16 | assert pynuodb.threadsafety == 1 17 | assert pynuodb.paramstyle == 'qmark' 18 | -------------------------------------------------------------------------------- /tests/nuodb_huge_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | (C) Copyright 2013-2025 Dassault Systemes SE. All Rights Reserved. 3 | 4 | This software is licensed under a BSD 3-Clause License. 5 | See the LICENSE file provided with this software. 6 | """ 7 | 8 | from . import nuodb_base 9 | 10 | 11 | class TestNuoDBHuge(nuodb_base.NuoBase): 12 | def test_wide_select(self): 13 | 14 | con = self._connect() 15 | cursor = con.cursor() 16 | total_columns = 5120 17 | 18 | alphabet = "ABCDEFGHIJKLMNOPQRSTUWXYZ" 19 | select_string = "SELECT " 20 | 21 | for col in range(1, total_columns + 1): 22 | select_string += "'" + alphabet + str(col) + "'" 23 | if col < total_columns: 24 | select_string += " , " 25 | else: 26 | select_string += " FROM DUAL" 27 | 28 | cursor.execute(select_string) 29 | row = cursor.fetchone() 30 | 31 | assert len(row) == total_columns 32 | 33 | for col in range(total_columns): 34 | assert row[col] == alphabet + str(col + 1) 35 | 36 | def test_wide_string(self): 37 | 38 | con = self._connect() 39 | cursor = con.cursor() 40 | 41 | total_width = 5120 42 | alphabet = "ABCDEFGHIJKLMNOPQRSTUWXYZ" 43 | alphabet_multi = "" 44 | 45 | for col in range(1, total_width + 1): 46 | alphabet_multi += alphabet 47 | 48 | select_string = "SELECT '" + alphabet_multi + "' , ? , '" + alphabet_multi + "' = ? FROM DUAL" 49 | 50 | cursor.execute(select_string, [alphabet_multi, alphabet_multi]) 51 | row = cursor.fetchone() 52 | assert len(row[0]) == total_width * len(alphabet) 53 | assert len(row[1]) == total_width * len(alphabet) 54 | assert row[2] 55 | 56 | def test_long_select(self): 57 | 58 | con = self._connect() 59 | cursor = con.cursor() 60 | 61 | cursor.execute("DROP TABLE IF EXISTS ten") 62 | cursor.execute("DROP SEQUENCE IF EXISTS s1") 63 | cursor.execute("DROP TABLE IF EXISTS huge_select") 64 | 65 | cursor.execute("CREATE TABLE ten (f1 INTEGER)") 66 | cursor.execute("INSERT INTO ten VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9),(10)") 67 | 68 | cursor.execute("CREATE SEQUENCE s1") 69 | cursor.execute("CREATE TABLE huge_select (f1 INTEGER GENERATED BY DEFAULT AS IDENTITY(s1))") 70 | cursor.execute( 71 | "INSERT INTO huge_select SELECT NEXT VALUE FOR s1 FROM ten AS a1,ten AS a2,ten AS a3,ten AS a4,ten AS a5, ten AS a6") 72 | 73 | cursor.execute("SELECT * FROM huge_select") 74 | 75 | total_rows = 0 76 | while (1): 77 | rows = cursor.fetchmany(10000) 78 | if rows is None or len(rows) == 0: 79 | break 80 | total_rows = total_rows + len(rows) 81 | 82 | assert total_rows == 1000000 83 | 84 | cursor.execute("DROP TABLE ten") 85 | cursor.execute("DROP TABLE huge_select") 86 | -------------------------------------------------------------------------------- /tests/nuodb_service_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | (C) Copyright 2013-2025 Dassault Systemes SE. All Rights Reserved. 3 | 4 | This software is licensed under a BSD 3-Clause License. 5 | See the LICENSE file provided with this software. 6 | """ 7 | 8 | import os 9 | import tempfile 10 | import gzip 11 | import xml.etree.ElementTree as ET 12 | import pytest 13 | import logging 14 | 15 | try: 16 | from typing import Optional # pylint: disable=unused-import 17 | except ImportError: 18 | pass 19 | 20 | try: 21 | from pynuoadmin import nuodb_mgmt 22 | __have_pynuoadmin = True 23 | except ImportError: 24 | __have_pynuoadmin = False 25 | 26 | import pynuodb 27 | 28 | from . import nuodb_base 29 | 30 | _log = logging.getLogger('pynuodbtest') 31 | 32 | 33 | @pytest.fixture(scope="session") 34 | def ap_conn(): 35 | # type: () -> Optional[nuodb_mgmt.AdminConnection] 36 | global __have_pynuoadmin 37 | if not __have_pynuoadmin: 38 | _log.info("Cannot load NuoDB pynuoadmin Python module") 39 | return None 40 | 41 | # Use the same method of locating the AP REST service as nuocmd. 42 | key = os.environ.get('NUOCMD_CLIENT_KEY') 43 | verify = os.environ.get('NUOCMD_VERIFY_SERVER') 44 | api = os.environ.get('NUOCMD_API_SERVER', 'localhost:8888') 45 | if not api.startswith('http://') and not api.startswith('https://'): 46 | if not key: 47 | api = 'http://' + api 48 | else: 49 | api = 'https://' + api 50 | 51 | _log.info("Creating AP connection to %s (client_key=%s verify=%s)", 52 | api, str(key), str(verify)) 53 | return nuodb_mgmt.AdminConnection(api, client_key=key, verify=verify) 54 | 55 | 56 | class TestNuoDBService(nuodb_base.NuoBase): 57 | """Test using the Session object to connect directly to an Engine.""" 58 | 59 | def test_query_memory(self, ap_conn): 60 | """Test query of process memory.""" 61 | if ap_conn is None: 62 | pytest.skip("No AP available") 63 | 64 | dbname = self.connect_args['database'] 65 | procs = ap_conn.get_processes(db_name=dbname) 66 | dbpasswd = ap_conn._get_db_password(dbname) 67 | 68 | def try_message(msg): 69 | session = pynuodb.session.Session( 70 | procs[0].address, service='Query', 71 | options={'verifyHostname': 'False'}) 72 | session.authorize('Cloud', dbpasswd) 73 | session.send(msg) 74 | res = session.recv() 75 | root = ET.fromstring(res) 76 | assert root.tag == 'MemoryInfo' 77 | info = root.findall('HeapInformation') 78 | assert len(info) == 1 79 | assert info[0].tag == 'HeapInformation' 80 | 81 | # Send with different types of buffers 82 | msg = '' 83 | try_message(msg) 84 | try_message(msg.encode('utf-8')) 85 | try_message(pynuodb.crypt.bytesToArray(msg.encode('utf-8'))) 86 | 87 | def test_request_gc(self, ap_conn): 88 | """Test a request operation.""" 89 | if ap_conn is None: 90 | pytest.skip("No AP available") 91 | 92 | dbname = self.connect_args['database'] 93 | procs = ap_conn.get_processes(db_name=dbname) 94 | dbpasswd = ap_conn._get_db_password(dbname) 95 | 96 | session = pynuodb.session.Session( 97 | procs[0].address, service='Admin', 98 | options={'verifyHostname': 'False'}) 99 | session.authorize('Cloud', dbpasswd) 100 | 101 | req = ET.fromstring(''' 102 | 100000 103 | ''') 104 | res = session.doRequest(children=[req]) 105 | root = ET.fromstring(res) 106 | assert root.tag == 'Response' 107 | info = root.findall('ChorusActionStarted') 108 | assert len(info) == 1 109 | assert info[0].get('Action') == 'RequestGarbageCollection' 110 | 111 | def test_stream_recv(self, ap_conn): 112 | """Test the stream_recv() facility.""" 113 | if ap_conn is None: 114 | pytest.skip("No AP available") 115 | 116 | dbname = self.connect_args['database'] 117 | procs = ap_conn.get_processes(db_name=dbname) 118 | dbpasswd = ap_conn._get_db_password(dbname) 119 | 120 | session = pynuodb.session.Session( 121 | procs[0].address, service='Admin', 122 | options={'verifyHostname': 'False'}) 123 | session.authorize('Cloud', dbpasswd) 124 | 125 | session.send(''' 126 | 127 | ''') 128 | resp = session.recv() 129 | xml = ET.fromstring(resp) 130 | assert xml.find('Success') is not None, "Failed: %s" % (resp) 131 | 132 | deppath = os.path.join(tempfile.gettempdir(), 'deps.tar.gz') 133 | with open(deppath, 'wb') as of: 134 | for data in session.stream_recv(): 135 | of.write(data) 136 | 137 | # The socket should be closed now: this will raise 138 | with pytest.raises(pynuodb.session.SessionException): 139 | session._sock 140 | 141 | # Now make sure that what we read is uncompressable 142 | with gzip.GzipFile(deppath, 'rb') as gz: 143 | # We don't really care we just want to make sure it works 144 | assert gz.read() is not None, "Failed to unzip %s" % (deppath) 145 | -------------------------------------------------------------------------------- /tests/nuodb_statement_management_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | (C) Copyright 2013-2025 Dassault Systemes SE. All Rights Reserved. 3 | 4 | This software is licensed under a BSD 3-Clause License. 5 | See the LICENSE file provided with this software. 6 | """ 7 | 8 | import decimal 9 | 10 | from . import nuodb_base 11 | 12 | 13 | class TestNuoDBStatementManagement(nuodb_base.NuoBase): 14 | def test_stable_statement(self): 15 | con = self._connect() 16 | cursor = con.cursor() 17 | init_handle = extract_statement_handle(cursor) 18 | cursor.execute("drop table typetest if exists") 19 | try: 20 | cursor.execute("create table typetest (id integer GENERATED ALWAYS AS IDENTITY, smallint_col smallint, " 21 | "integer_col integer, bigint_col bigint, numeric_col numeric(10, 2), " 22 | "decimal_col decimal(10, 2), double_col double precision)") 23 | 24 | cursor.execute("insert into typetest (smallint_col, integer_col, bigint_col, numeric_col, decimal_col, " 25 | "double_col) values (0, 0, 0, 0, 0, 0)") 26 | 27 | con.commit() 28 | 29 | cursor.execute("select * from typetest order by id desc limit 1") 30 | row = cursor.fetchone() 31 | 32 | for i in range(1, len(row)): 33 | assert row[i] == 0 34 | 35 | current_handle = extract_statement_handle(cursor) 36 | assert init_handle == current_handle 37 | 38 | finally: 39 | try: 40 | cursor.execute("drop table typetest if exists") 41 | finally: 42 | con.close() 43 | 44 | def test_statement_per_cursor(self): 45 | con = self._connect() 46 | try: 47 | cursor1 = con.cursor() 48 | cursor2 = con.cursor() 49 | cursor3 = con.cursor() 50 | 51 | assert extract_statement_handle(cursor1) != extract_statement_handle(cursor2) 52 | assert extract_statement_handle(cursor2) != extract_statement_handle(cursor3) 53 | assert extract_statement_handle(cursor1) != extract_statement_handle(cursor3) 54 | 55 | finally: 56 | con.close() 57 | 58 | def test_prepared_statement_cache(self): 59 | con = self._connect() 60 | cursor = con.cursor() 61 | cursor.execute("drop table typetest if exists") 62 | try: 63 | cursor.execute("create table typetest (id integer GENERATED ALWAYS AS IDENTITY, smallint_col smallint, " 64 | "integer_col integer, bigint_col bigint, numeric_col numeric(10, 2), " 65 | "decimal_col decimal(10, 2), double_col double)") 66 | 67 | test_vals = (3424, 23453464, 45453453454545, decimal.Decimal('234355.33'), decimal.Decimal('976.2'), 68 | 10000.999) 69 | query = "insert into typetest (smallint_col, integer_col, bigint_col, numeric_col, decimal_col, " \ 70 | "double_col) values (?, ?, ?, ?, ?, ?)" 71 | 72 | cursor.execute(query, test_vals) 73 | 74 | cursor.execute("select * from typetest order by id desc limit 1") 75 | row = cursor.fetchone() 76 | 77 | for i in range(1, len(row)): 78 | assert row[i] == test_vals[i - 1] 79 | 80 | ps_cache = extract_prepared_statement_dict(cursor) 81 | assert len(ps_cache) == 1 82 | assert query in ps_cache 83 | 84 | finally: 85 | try: 86 | cursor.execute("drop table typetest if exists") 87 | finally: 88 | con.close() 89 | 90 | def test_prepared_statement_cache_should_not_grow(self): 91 | con = self._connect() 92 | cursor = con.cursor() 93 | cursor.execute("drop table typetest if exists") 94 | try: 95 | cursor.execute("create table typetest (id integer GENERATED ALWAYS AS IDENTITY, smallint_col smallint, " 96 | "integer_col integer, bigint_col bigint, numeric_col numeric(10, 2), " 97 | "decimal_col decimal(10, 2), double_col double)") 98 | 99 | test_vals = (3424, 23453464, 45453453454545, decimal.Decimal('234355.33'), decimal.Decimal('976.2'), 100 | 10000.999) 101 | query = "insert into typetest (smallint_col, integer_col, bigint_col, numeric_col, decimal_col, " \ 102 | "double_col) values (?, ?, ?, ?, ?, ?)" 103 | 104 | for _ in range(0, 20): 105 | cursor.execute(query, test_vals) 106 | 107 | cursor.execute("select * from typetest order by id desc limit 1") 108 | row = cursor.fetchone() 109 | 110 | for i in range(1, len(row)): 111 | assert row[i] == test_vals[i - 1] 112 | 113 | ps_cache = extract_prepared_statement_dict(cursor) 114 | assert len(ps_cache) == 1 115 | assert query in ps_cache 116 | 117 | finally: 118 | try: 119 | cursor.execute("drop table typetest if exists") 120 | finally: 121 | con.close() 122 | 123 | def test_prepared_statement_cache_stable(self): 124 | con = self._connect() 125 | cursor = con.cursor() 126 | cursor.execute("drop table typetest if exists") 127 | try: 128 | cursor.execute("create table typetest (id integer GENERATED ALWAYS AS IDENTITY, smallint_col smallint, " 129 | "integer_col integer, bigint_col bigint, numeric_col numeric(10, 2), " 130 | "decimal_col decimal(10, 2), double_col double)") 131 | 132 | test_vals = (3424, 23453464, 45453453454545, decimal.Decimal('234355.33'), decimal.Decimal('976.2'), 133 | 10000.999) 134 | query = "insert into typetest (smallint_col, integer_col, bigint_col, numeric_col, decimal_col, " \ 135 | "double_col) values (?, ?, ?, ?, ?, ?)" 136 | 137 | handle = None 138 | for _ in range(0, 20): 139 | cursor.execute(query, test_vals) 140 | 141 | cursor.execute("select * from typetest order by id desc limit 1") 142 | row = cursor.fetchone() 143 | 144 | for i in range(1, len(row)): 145 | assert row[i] == test_vals[i - 1] 146 | 147 | ps_cache = extract_prepared_statement_dict(cursor) 148 | assert len(ps_cache) == 1 149 | assert query in ps_cache 150 | if handle is None: 151 | handle = ps_cache[query].handle 152 | else: 153 | assert handle == ps_cache[query].handle 154 | 155 | finally: 156 | try: 157 | cursor.execute("drop table typetest if exists") 158 | finally: 159 | con.close() 160 | 161 | def test_prepared_statement_cache_should_grow(self): 162 | con = self._connect() 163 | cursor = con.cursor() 164 | cursor.execute("drop table typetest if exists") 165 | try: 166 | cursor.execute("create table typetest (id integer GENERATED ALWAYS AS IDENTITY, smallint_col smallint, " 167 | "integer_col integer, bigint_col bigint, numeric_col numeric(10, 2), " 168 | "decimal_col decimal(10, 2), double_col double)") 169 | 170 | test_vals = (3424, 23453464, 45453453454545, decimal.Decimal('234355.33'), decimal.Decimal('976.2'), 171 | 10000.999) 172 | 173 | queries = ["insert into typetest (smallint_col, integer_col, bigint_col, numeric_col, decimal_col, " 174 | "double_col) values (?, ?, ?, ?, ?, ?)", 175 | "insert into typetest (smallint_col, integer_col, bigint_col, numeric_col, decimal_col) values " 176 | "(?, ?, ?, ?, ?)", 177 | "insert into typetest (smallint_col, integer_col, bigint_col, numeric_col) values (?, ?, ?, ?)", 178 | "insert into typetest (smallint_col, integer_col, bigint_col) values (?, ?, ?)", 179 | "insert into typetest (smallint_col, integer_col) values (?, ?)", 180 | "insert into typetest (smallint_col) values (?)"] 181 | 182 | for _ in range(0, 10): 183 | for i in range(0, len(queries)): 184 | cursor.execute(queries[i], test_vals[0:len(queries) - i]) 185 | 186 | ps_cache = extract_prepared_statement_dict(cursor) 187 | assert len(queries) == len(ps_cache) 188 | for query in queries: 189 | assert query in ps_cache 190 | 191 | finally: 192 | try: 193 | cursor.execute("drop table typetest if exists") 194 | finally: 195 | con.close() 196 | 197 | def test_prepared_statement_cache_eviction(self): 198 | con = self._connect() 199 | cache_size = 5 200 | cursor = con.cursor(prepared_statement_cache_size=cache_size) 201 | cursor.execute("drop table typetest if exists") 202 | try: 203 | cursor.execute("create table typetest (id integer GENERATED ALWAYS AS IDENTITY, smallint_col smallint, " 204 | "integer_col integer, bigint_col bigint, numeric_col numeric(10, 2), " 205 | "decimal_col decimal(10, 2), double_col double)") 206 | 207 | test_vals = (3424, 23453464, 45453453454545, decimal.Decimal('234355.33'), decimal.Decimal('976.2'), 208 | 10000.999) 209 | 210 | queries = ["insert into typetest (smallint_col, integer_col, bigint_col, numeric_col, decimal_col, " 211 | "double_col) values (?, ?, ?, ?, ?, ?)", 212 | "insert into typetest (smallint_col, integer_col, bigint_col, numeric_col, decimal_col) values " 213 | "(?, ?, ?, ?, ?)", 214 | "insert into typetest (smallint_col, integer_col, bigint_col, numeric_col) values (?, ?, ?, ?)", 215 | "insert into typetest (smallint_col, integer_col, bigint_col) values (?, ?, ?)", 216 | "insert into typetest (smallint_col, integer_col) values (?, ?)", 217 | "insert into typetest (smallint_col) values (?)"] 218 | 219 | for i in range(0, len(queries)): 220 | cursor.execute(queries[i], test_vals[0:len(queries) - i]) 221 | 222 | ps_cache = extract_prepared_statement_dict(cursor) 223 | assert cache_size == len(ps_cache) 224 | for query in queries[len(queries) - cache_size:]: 225 | assert query in ps_cache 226 | 227 | finally: 228 | try: 229 | cursor.execute("drop table typetest if exists") 230 | finally: 231 | con.close() 232 | 233 | def test_prepared_statement_cache_eviction_lru(self): 234 | con = self._connect() 235 | cache_size = 4 236 | cursor = con.cursor(prepared_statement_cache_size=cache_size) 237 | cursor.execute("drop table typetest if exists") 238 | try: 239 | cursor.execute("create table typetest (id integer GENERATED ALWAYS AS IDENTITY, smallint_col smallint, " 240 | "integer_col integer, bigint_col bigint, numeric_col numeric(10, 2), " 241 | "decimal_col decimal(10, 2), double_col double)") 242 | 243 | test_vals = (3424, 23453464, 45453453454545, decimal.Decimal('234355.33'), decimal.Decimal('976.2'), 244 | 10000.999) 245 | 246 | queries = ["insert into typetest (smallint_col, integer_col, bigint_col, numeric_col, decimal_col, " 247 | "double_col) values (?, ?, ?, ?, ?, ?)", 248 | "insert into typetest (smallint_col, integer_col, bigint_col, numeric_col, decimal_col) values " 249 | "(?, ?, ?, ?, ?)", 250 | "insert into typetest (smallint_col, integer_col, bigint_col, numeric_col) values (?, ?, ?, ?)", 251 | "insert into typetest (smallint_col, integer_col, bigint_col) values (?, ?, ?)", 252 | "insert into typetest (smallint_col, integer_col) values (?, ?)", 253 | "insert into typetest (smallint_col) values (?)"] 254 | 255 | query_order = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 256 | 3, 1, 4, 5, 1, 4, 2, 3, 1, 5, 4, 3, 5, 1, 257 | 5, 1, 5, 3, 1, 2, 1, 1] 258 | for i in query_order: 259 | cursor.execute(queries[i], test_vals[0:len(queries) - i]) 260 | 261 | ps_cache = extract_prepared_statement_dict(cursor) 262 | assert cache_size == len(ps_cache) 263 | for query in [queries[1], queries[2], queries[3], queries[5]]: 264 | assert query in ps_cache 265 | 266 | for query in [queries[0], queries[4]]: 267 | assert query not in ps_cache 268 | 269 | finally: 270 | try: 271 | cursor.execute("drop table typetest if exists") 272 | finally: 273 | con.close() 274 | 275 | 276 | def extract_statement_handle(cursor): 277 | return cursor._statement_cache._statement.handle 278 | 279 | 280 | def extract_prepared_statement_dict(cursor): 281 | return cursor._statement_cache._ps_cache 282 | -------------------------------------------------------------------------------- /tests/nuodb_transaction_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | (C) Copyright 2013-2025 Dassault Systemes SE. All Rights Reserved. 3 | 4 | This software is licensed under a BSD 3-Clause License. 5 | See the LICENSE file provided with this software. 6 | """ 7 | 8 | from . import nuodb_base 9 | 10 | 11 | class TestNuoDBTransaction(nuodb_base.NuoBase): 12 | def test_connection_isolation(self): 13 | con1 = self._connect() 14 | con2 = self._connect() 15 | 16 | cursor1 = con1.cursor() 17 | cursor2 = con2.cursor() 18 | 19 | cursor1.execute("SELECT 1 FROM DUAL UNION ALL SELECT 2 FROM DUAL") 20 | cursor2.execute("SELECT 3 FROM DUAL UNION ALL SELECT 4 FROM DUAL") 21 | 22 | assert cursor1.fetchone()[0] == 1 23 | assert cursor2.fetchone()[0] == 3 24 | 25 | assert cursor1.fetchone()[0] == 2 26 | assert cursor2.fetchone()[0] == 4 27 | 28 | def test_cursor_isolation(self): 29 | con = self._connect() 30 | 31 | cursor1 = con.cursor() 32 | cursor2 = con.cursor() 33 | 34 | cursor1.execute("SELECT 1 FROM DUAL UNION ALL SELECT 2 FROM DUAL") 35 | cursor2.execute("SELECT 3 FROM DUAL UNION ALL SELECT 4 FROM DUAL") 36 | 37 | assert cursor1.fetchone()[0] == 1 38 | assert cursor2.fetchone()[0] == 3 39 | 40 | assert cursor1.fetchone()[0] == 2 41 | assert cursor2.fetchone()[0] == 4 42 | 43 | def test_rollback(self): 44 | con = self._connect() 45 | cursor = con.cursor() 46 | 47 | cursor.execute("DROP TABLE IF EXISTS rollback_table") 48 | cursor.execute("CREATE TABLE rollback_table (f1 integer)") 49 | 50 | con.commit() 51 | 52 | cursor.execute("INSERT INTO rollback_table VALUES (1)") 53 | con.rollback() 54 | 55 | cursor.execute("SELECT COUNT(*) FROM rollback_table") 56 | assert cursor.fetchone()[0] == 0 57 | 58 | cursor.execute("DROP TABLE rollback_table") 59 | 60 | def test_commit(self): 61 | con1 = self._connect() 62 | con2 = self._connect() 63 | 64 | cursor1 = con1.cursor() 65 | cursor2 = con2.cursor() 66 | 67 | cursor1.execute("DROP TABLE IF EXISTS commit_table") 68 | cursor1.execute("CREATE TABLE commit_table (f1 integer)") 69 | 70 | con1.commit() 71 | 72 | cursor1.execute("INSERT INTO commit_table VALUES (1)") 73 | 74 | cursor2.execute("SELECT COUNT(*) FROM commit_table") 75 | assert cursor2.fetchone()[0] == 0 76 | 77 | con1.commit() 78 | con2.commit() 79 | 80 | cursor2.execute("SELECT COUNT(*) FROM commit_table") 81 | assert cursor2.fetchone()[0] == 1 82 | 83 | cursor1.execute("DROP TABLE commit_table") 84 | 85 | def test_rollback_disconnect(self): 86 | con1 = self._connect() 87 | cursor1 = con1.cursor() 88 | 89 | cursor1.execute("DROP TABLE IF EXISTS rollback_disconnect") 90 | cursor1.execute("CREATE TABLE rollback_disconnect (f1 integer)") 91 | 92 | con1.commit() 93 | 94 | cursor1.execute("INSERT INTO rollback_disconnect VALUES (1)") 95 | con1.close() 96 | 97 | con2 = self._connect() 98 | cursor2 = con2.cursor() 99 | cursor2.execute("SELECT COUNT(*) FROM rollback_disconnect") 100 | assert cursor2.fetchone()[0] == 0 101 | 102 | cursor2.execute("DROP TABLE rollback_disconnect") 103 | 104 | def test_autocommit_set(self): 105 | con1 = self._connect() 106 | con2 = self._connect() 107 | 108 | assert not con1.auto_commit 109 | 110 | con1.auto_commit = True 111 | assert con1.auto_commit 112 | 113 | con2.auto_commit = True 114 | assert con2.auto_commit 115 | 116 | cursor1 = con1.cursor() 117 | cursor1.execute("DROP TABLE IF EXISTS autocommit_set") 118 | cursor1.execute("CREATE TABLE autocommit_set (f1 integer)") 119 | 120 | cursor1.execute("INSERT INTO autocommit_set VALUES (1)") 121 | 122 | cursor2 = con2.cursor() 123 | cursor2.execute("SELECT COUNT(*) FROM autocommit_set") 124 | assert cursor2.fetchone()[0] == 1 125 | cursor2.execute("TRUNCATE TABLE autocommit_set") 126 | 127 | con1.auto_commit = False 128 | assert not con1.auto_commit 129 | 130 | cursor1.execute("INSERT INTO autocommit_set VALUES (1)") 131 | cursor2.execute("SELECT COUNT(*) FROM autocommit_set") 132 | assert cursor2.fetchone()[0] == 0 133 | 134 | con1.commit() 135 | cursor2.execute("SELECT COUNT(*) FROM autocommit_set") 136 | assert cursor2.fetchone()[0] == 1 137 | 138 | cursor1.execute("DROP TABLE autocommit_set") 139 | -------------------------------------------------------------------------------- /tests/nuodb_types_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | (C) Copyright 2025 Dassault Systemes SE. All Rights Reserved. 3 | 4 | This software is licensed under a BSD 3-Clause License. 5 | See the LICENSE file provided with this software. 6 | """ 7 | 8 | import decimal 9 | import datetime 10 | 11 | from . import nuodb_base 12 | 13 | 14 | class TestNuoDBTypes(nuodb_base.NuoBase): 15 | def test_boolean_types(self): 16 | con = self._connect() 17 | cursor = con.cursor() 18 | 19 | cursor.execute("CREATE TEMPORARY TABLE tmp (v1 BOOLEAN)") 20 | cursor.execute("INSERT INTO tmp VALUES (true)") 21 | 22 | cursor.execute("SELECT * FROM tmp") 23 | row = cursor.fetchone() 24 | 25 | assert len(row) == 1 26 | assert row[0] 27 | 28 | def test_string_types(self): 29 | con = self._connect() 30 | cursor = con.cursor() 31 | 32 | cursor.execute("CREATE TEMPORARY TABLE tmp (" 33 | " v1 STRING," 34 | " v2 CHARACTER," 35 | " v3 CHARACTER LARGE OBJECT)") 36 | cursor.execute("INSERT INTO tmp VALUES ('simple', 'a', 'clob')") 37 | 38 | cursor.execute("SELECT * FROM tmp") 39 | row = cursor.fetchone() 40 | 41 | assert len(row) == 3 42 | assert row[0] == 'simple' 43 | assert row[1] == 'a' 44 | assert row[2] == 'clob' 45 | 46 | def test_numeric_types(self): 47 | con = self._connect() 48 | cursor = con.cursor() 49 | 50 | cursor.execute("CREATE TEMPORARY TABLE tmp (" 51 | " v1 SMALLINT," 52 | " v2 INTEGER," 53 | " v3 BIGINT," 54 | " v4 NUMERIC(30, 1)," 55 | " v5 FLOAT," 56 | " v6 DOUBLE)") 57 | 58 | cursor.execute("INSERT INTO tmp VALUES (1, 2, 9223372036854775807," 59 | " 9223372036854775807111.5, 5.6, 7.8)") 60 | 61 | cursor.execute("SELECT * FROM tmp") 62 | row = cursor.fetchone() 63 | 64 | assert len(row) == 6 65 | assert row[0] == 1 66 | assert row[1] == 2 67 | assert row[2] == 9223372036854775807 68 | assert row[3] == decimal.Decimal('9223372036854775807111.5') 69 | assert row[4] == 5.6 70 | assert row[5] == 7.8 71 | 72 | def test_binary_types(self): 73 | con = self._connect() 74 | cursor = con.cursor() 75 | 76 | cursor.execute("CREATE TEMPORARY TABLE tmp (" 77 | " v1 BLOB," 78 | " v2 BINARY," 79 | " v3 BINARY VARYING(10))") 80 | 81 | cursor.execute("INSERT INTO tmp VALUES ('foo', 'a', 'barbaz')") 82 | 83 | cursor.execute("SELECT * FROM tmp") 84 | row = cursor.fetchone() 85 | 86 | assert len(row) == 3 87 | assert row[0] == b'foo' 88 | assert row[1] == b'a' 89 | assert row[2] == b'barbaz' 90 | 91 | def test_datetime_types(self): 92 | con = self._connect() 93 | cursor = con.cursor() 94 | 95 | cursor.execute("CREATE TEMPORARY TABLE tmp (" 96 | " v1 DATE," 97 | " v2 TIME," 98 | " v3 TIMESTAMP," 99 | " v4 TIMESTAMP WITHOUT TIME ZONE)") 100 | 101 | cursor.execute("INSERT INTO tmp VALUES (" 102 | " '1/1/2000'," 103 | " '05:44:33.2211'," 104 | " '1/1/2000 05:44:33.2211'," 105 | " '1/1/2000 05:44:33.2211')") 106 | 107 | cursor.execute("SELECT * FROM tmp") 108 | row = cursor.fetchone() 109 | 110 | assert len(row) == 4 111 | assert row[0] == datetime.date(2000, 1, 1) 112 | assert row[1] == datetime.time(5, 44, 33, 221100) 113 | assert row[2] == datetime.datetime(2000, 1, 1, 5, 44, 33, 221100) 114 | assert row[3] == datetime.datetime(2000, 1, 1, 5, 44, 33, 221100) 115 | 116 | def test_null_type(self): 117 | con = self._connect() 118 | cursor = con.cursor() 119 | 120 | null_type = self.driver.TypeObjectFromNuodb('') 121 | 122 | cursor.execute("SELECT NULL from dual") 123 | row = cursor.fetchone() 124 | 125 | assert len(row) == 1 126 | assert cursor.description[0][1] == null_type 127 | assert row[0] is None 128 | -------------------------------------------------------------------------------- /tests/sample.py: -------------------------------------------------------------------------------- 1 | """This assumes that you have the quickstart database running (test@localhost). 2 | 3 | If you don't, start it by running: /opt/nuodb/samples/nuoadmin-quickstart 4 | """ 5 | 6 | import os 7 | 8 | import pynuodb 9 | 10 | options = {"schema": "hockey"} 11 | 12 | port = os.environ.get('NUODB_PORT') 13 | host = 'localhost' + (':' + port if port else '') 14 | 15 | trustStore = os.environ.get('NUOCMD_VERIFY_SERVER') 16 | if trustStore: 17 | options['trustStore'] = trustStore 18 | options['verifyHostname'] = 'false' 19 | 20 | connection = pynuodb.connect("test", host, "dba", "goalie", options=options) 21 | 22 | cursor = connection.cursor() 23 | cursor.arraysize = 3 24 | cursor.execute("select * from hockey") 25 | print(cursor.fetchone()) 26 | --------------------------------------------------------------------------------