From cbdd8c4f694cb972c9578713295aa948a7dc413b Mon Sep 17 00:00:00 2001 From: wellwelwel <46850407+wellwelwel@users.noreply.github.com> Date: Tue, 17 Feb 2026 19:48:13 -0300 Subject: [PATCH 1/3] refactor: cjs to mts --- .prettierrc | 24 +- eslint.config.mjs | 13 +- package-lock.json | 658 ++++++++++++++---- package.json | 9 +- test/docker-compose.yml | 2 + test/esm/common.test.mts | 32 +- .../config/test-connect-timeout.test.mts | 41 ++ .../test-typecast-global-false.test.mts | 35 + .../test-typecast-global-option.test.mts | 48 ++ .../encoding/test-charset-results.test.mts | 93 +++ .../encoding/test-client-encodings.test.mts | 45 ++ .../encoding/test-non-bmp-chars.test.mts | 36 + .../encoding/test-track-encodings.test.mts | 30 + .../test-aurora-failover-readonly.test.mts | 181 +++++ ...est-backpressure-load-data-infile.test.mts | 110 +++ ...est-backpressure-result-streaming.test.mts | 60 ++ .../test-binary-charset-string.test.mts | 96 +++ .../connection/test-binary-longlong.test.mts | 125 ++++ .../test-binary-multiple-results.test.mts | 223 ++++++ .../test-binary-notnull-nulls.test.mts | 71 ++ .../connection/test-buffer-params.test.mts | 43 ++ .../test-change-user-multi-factor.test.mts | 161 +++++ .../test-change-user-plugin-auth.test.mts | 85 +++ .../connection/test-change-user.test.mts | 85 +++ .../connection/test-charset-encoding.test.mts | 64 ++ .../connection/test-column-inspect.test.mts | 4 +- ...st-connect-after-connection-error.test.mts | 47 ++ .../test-connect-after-connection.test.mts | 23 + ...t-connect-connection-closed-error.test.mts | 34 + .../connection/test-connect-sha1.test.mts | 93 +++ .../test-connect-time-error.test.mts | 41 ++ .../connection/test-connect-with-uri.test.mts | 30 + ...st-connection-reset-while-closing.test.mts | 29 + .../test-custom-date-parameter.test.mts | 36 + .../connection/test-date-parameter.test.mts | 25 + .../connection/test-datetime.test.mts | 337 +++++++++ .../test-decimals-as-numbers.test.mts | 38 + .../connection/test-disconnects.test.mts | 89 +++ .../connection/test-error-events.test.mts | 34 + .../connection/test-errors.test.mts | 61 ++ .../connection/test-execute-1.test.mts | 2 +- .../test-execute-and-unprepare.test.mts | 25 + .../test-execute-bind-boolean.test.mts | 23 + .../test-execute-bind-date.test.mts | 24 + .../test-execute-bind-function.test.mts | 29 + .../test-execute-bind-json.test.mts | 25 + .../test-execute-bind-null.test.mts | 25 + .../test-execute-bind-number.test.mts | 30 + .../test-execute-bind-undefined.test.mts | 29 + .../connection/test-execute-cached.test.mts | 47 ++ .../test-execute-newdecimal.test.mts | 27 + .../test-execute-nocolumndef.test.mts | 230 ++++++ .../test-execute-null-bitmap.test.mts | 29 + .../connection/test-execute-order.test.mts | 24 + .../connection/test-execute-signed.test.mts | 44 ++ .../test-execute-type-casting.test.mts | 99 +++ ...-insert-bigint-big-number-strings.test.mts | 72 ++ .../connection/test-insert-bigint.test.mts | 81 +++ .../connection/test-insert-json.test.mts | 39 ++ .../test-insert-large-blob.test.mts | 98 +++ .../test-insert-negative-ai.test.mts | 58 ++ .../connection/test-insert-results.test.mts | 50 ++ .../test-invalid-date-result.test.mts | 66 ++ .../connection/test-load-infile.test.mts | 114 +++ .../connection/test-multiple-results.test.mts | 237 +++++++ .../test-named-placeholders.test.mts | 105 +++ .../test-nested-tables-query.test.mts | 172 +++++ .../connection/test-null-buffer.test.mts | 37 + .../connection/test-null-double.test.mts | 25 + .../connection/test-null-int.test.mts | 25 + .../integration/connection/test-null.test.mts | 36 + .../test-parameters-questionmark.test.mts | 34 + .../test-prepare-and-close.test.mts | 32 + .../connection/test-prepare-simple.test.mts | 41 ++ .../test-prepare-then-execute.test.mts | 43 ++ .../connection/test-protocol-errors.test.mts | 86 +++ .../connection/test-query-timeout.test.mts | 101 +++ .../connection/test-query-zero.test.mts | 19 + .../integration/connection/test-quit.test.mts | 82 +++ .../connection/test-select-1.test.mts | 22 + .../test-select-empty-string.test.mts | 21 + .../connection/test-select-json.test.mts | 39 ++ .../connection/test-select-negative.test.mts | 29 + .../connection/test-select-ssl.test.mts | 40 ++ .../connection/test-select-utf8.test.mts | 23 + .../connection/test-server-listen.test.mts | 42 ++ .../connection/test-signed-tinyint.test.mts | 33 + ...t-stream-error-destroy-connection.test.mts | 100 +++ .../connection/test-stream-errors.test.mts | 98 +++ .../connection/test-stream.test.mts | 83 +++ .../connection/test-then-on-query.test.mts | 22 + .../connection/test-timestamp.test.mts | 62 ++ .../test-track-state-change.test.mts | 49 ++ .../test-transaction-commit.test.mts | 49 ++ .../test-transaction-rollback.test.mts | 44 ++ ...est-type-cast-null-fields-execute.test.mts | 55 ++ .../test-type-cast-null-fields.test.mts | 49 ++ .../test-type-casting-execute.test.mts | 134 ++++ .../connection/test-type-casting.test.mts | 134 ++++ .../connection/test-typecast-execute.test.mts | 157 +++++ .../test-typecast-geometry-execute.test.mts | 52 ++ .../test-typecast-geometry.test.mts | 52 ++ ...test-typecast-overwriting-execute.test.mts | 52 ++ .../test-typecast-overwriting.test.mts | 52 ++ .../connection/test-typecast.test.mts | 159 +++++ .../test-update-changed-rows.test.mts | 66 ++ .../connection/test-vector.test.mts | 9 +- .../connection/type-casting-tests.test.mts | 221 ++++++ .../test-end-with-default-config.test.mts | 34 + ...test-end-with-graceful-end-config.test.mts | 29 + ...-release-idle-with-default-config.test.mts | 40 ++ ...ase-idle-with-graceful-end-config.test.mts | 40 ++ .../integration/named-placeholders.test.mts | 18 +- .../parsers/execute-results-creation.test.mts | 11 +- .../integration/parsers/json-parse.test.mts | 9 +- .../integration/parsers/json-string.test.mts | 9 +- .../parsers/query-results-creation.test.mts | 9 +- .../parsers/typecast-field-datetime.test.mts | 12 +- .../parsers/typecast-field-string.test.mts | 35 +- .../test-promise-wrapper.test.mts | 10 +- .../test-async-stack.test.mts | 56 ++ .../test-promise-wrappers.test.mts | 537 ++++++++++++++ .../regressions/test-#433.test.mts | 94 +++ .../regressions/test-#442.test.mts | 61 ++ .../regressions/test-#485.test.mts | 42 ++ .../regressions/test-#617.test.mts | 77 ++ .../regressions/test-#629.test.mts | 79 +++ .../integration/regressions/test-#82.test.mts | 63 ++ .../test-auth-switch-multi-factor.test.mts | 169 +++++ ...st-auth-switch-plugin-async-error.test.mts | 102 +++ .../test-auth-switch-plugin-error.test.mts | 100 +++ .../esm/integration/test-auth-switch.test.mts | 150 ++++ ...st-handshake-unknown-packet-error.test.mts | 95 +++ .../test-multi-result-streaming.test.mts | 52 ++ .../test-pool-connect-error.test.mts | 57 ++ .../integration/test-pool-disconnect.test.mts | 73 ++ test/esm/integration/test-pool-end.test.mts | 25 + ...release-idle-connection-replicate.test.mts | 81 +++ ...l-release-idle-connection-timeout.test.mts | 62 ++ ...test-pool-release-idle-connection.test.mts | 63 ++ .../integration/test-pool-release.test.mts | 41 ++ test/esm/integration/test-pool.test.mts | 2 +- .../integration/test-rows-as-array.test.mts | 67 ++ .../integration/test-server-close.test.mts | 54 ++ test/esm/regressions/2052.test.mts | 8 +- test/esm/tsconfig.json | 12 +- test/esm/unit/check-extensions.test.mts | 2 +- test/esm/unit/commands/test-query.test.mts | 18 + test/esm/unit/commands/test-quit.test.mts | 7 + .../connection/test-connection-state.test.mts | 156 +++++ .../test-connection_config.test.mts | 76 ++ .../packets/test-column-definition.test.mts | 81 +++ test/esm/unit/packets/test-datetime.test.mts | 28 + .../esm/unit/packets/test-ok-autoinc.test.mts | 14 + .../packets/test-ok-sessiontrack.test.mts | 38 + test/esm/unit/packets/test-text-row.test.mts | 25 + test/esm/unit/packets/test-time.test.mts | 20 + ...mbers-strings-binary-sanitization.test.mts | 10 +- ...numbers-strings-text-sanitization.test.mts | 7 +- .../parsers/cache-key-serialization.test.mts | 57 +- .../ensure-safe-binary-fields.test.mts | 4 +- .../parsers/ensure-safe-text-fields.test.mts | 4 +- ...t-big-numbers-binary-sanitization.test.mts | 7 +- ...ort-big-numbers-text-sanitization.test.mts | 7 +- .../unit/parsers/test-text-parser.test.mts | 49 ++ .../timezone-binary-sanitization.test.mts | 2 +- .../timezone-text-sanitization.test.mts | 2 +- .../test-connection-error-remove.test.mts | 88 +++ .../test-connection-order.test.mts | 48 ++ .../test-connection-retry.test.mts | 71 ++ .../pool-cluster/test-connection-rr.test.mts | 48 ++ .../esm/unit/pool-cluster/test-query.test.mts | 32 + .../pool-cluster/test-remove-by-name.test.mts | 79 +++ .../test-remove-by-pattern.test.mts | 70 ++ .../pool-cluster/test-restore-events.test.mts | 100 +++ .../unit/pool-cluster/test-restore.test.mts | 89 +++ test/esm/unit/protocol/SqlString.test.mts | 2 +- test/esm/unit/test-packet-parser.test.mts | 150 ++++ test/globals.d.ts | 9 + 179 files changed, 11252 insertions(+), 230 deletions(-) create mode 100644 test/esm/integration/config/test-connect-timeout.test.mts create mode 100644 test/esm/integration/config/test-typecast-global-false.test.mts create mode 100644 test/esm/integration/config/test-typecast-global-option.test.mts create mode 100644 test/esm/integration/connection/encoding/test-charset-results.test.mts create mode 100644 test/esm/integration/connection/encoding/test-client-encodings.test.mts create mode 100644 test/esm/integration/connection/encoding/test-non-bmp-chars.test.mts create mode 100644 test/esm/integration/connection/encoding/test-track-encodings.test.mts create mode 100644 test/esm/integration/connection/test-aurora-failover-readonly.test.mts create mode 100644 test/esm/integration/connection/test-backpressure-load-data-infile.test.mts create mode 100644 test/esm/integration/connection/test-backpressure-result-streaming.test.mts create mode 100644 test/esm/integration/connection/test-binary-charset-string.test.mts create mode 100644 test/esm/integration/connection/test-binary-longlong.test.mts create mode 100644 test/esm/integration/connection/test-binary-multiple-results.test.mts create mode 100644 test/esm/integration/connection/test-binary-notnull-nulls.test.mts create mode 100644 test/esm/integration/connection/test-buffer-params.test.mts create mode 100644 test/esm/integration/connection/test-change-user-multi-factor.test.mts create mode 100644 test/esm/integration/connection/test-change-user-plugin-auth.test.mts create mode 100644 test/esm/integration/connection/test-change-user.test.mts create mode 100644 test/esm/integration/connection/test-charset-encoding.test.mts create mode 100644 test/esm/integration/connection/test-connect-after-connection-error.test.mts create mode 100644 test/esm/integration/connection/test-connect-after-connection.test.mts create mode 100644 test/esm/integration/connection/test-connect-connection-closed-error.test.mts create mode 100644 test/esm/integration/connection/test-connect-sha1.test.mts create mode 100644 test/esm/integration/connection/test-connect-time-error.test.mts create mode 100644 test/esm/integration/connection/test-connect-with-uri.test.mts create mode 100644 test/esm/integration/connection/test-connection-reset-while-closing.test.mts create mode 100644 test/esm/integration/connection/test-custom-date-parameter.test.mts create mode 100644 test/esm/integration/connection/test-date-parameter.test.mts create mode 100644 test/esm/integration/connection/test-datetime.test.mts create mode 100644 test/esm/integration/connection/test-decimals-as-numbers.test.mts create mode 100644 test/esm/integration/connection/test-disconnects.test.mts create mode 100644 test/esm/integration/connection/test-error-events.test.mts create mode 100644 test/esm/integration/connection/test-errors.test.mts create mode 100644 test/esm/integration/connection/test-execute-and-unprepare.test.mts create mode 100644 test/esm/integration/connection/test-execute-bind-boolean.test.mts create mode 100644 test/esm/integration/connection/test-execute-bind-date.test.mts create mode 100644 test/esm/integration/connection/test-execute-bind-function.test.mts create mode 100644 test/esm/integration/connection/test-execute-bind-json.test.mts create mode 100644 test/esm/integration/connection/test-execute-bind-null.test.mts create mode 100644 test/esm/integration/connection/test-execute-bind-number.test.mts create mode 100644 test/esm/integration/connection/test-execute-bind-undefined.test.mts create mode 100644 test/esm/integration/connection/test-execute-cached.test.mts create mode 100644 test/esm/integration/connection/test-execute-newdecimal.test.mts create mode 100644 test/esm/integration/connection/test-execute-nocolumndef.test.mts create mode 100644 test/esm/integration/connection/test-execute-null-bitmap.test.mts create mode 100644 test/esm/integration/connection/test-execute-order.test.mts create mode 100644 test/esm/integration/connection/test-execute-signed.test.mts create mode 100644 test/esm/integration/connection/test-execute-type-casting.test.mts create mode 100644 test/esm/integration/connection/test-insert-bigint-big-number-strings.test.mts create mode 100644 test/esm/integration/connection/test-insert-bigint.test.mts create mode 100644 test/esm/integration/connection/test-insert-json.test.mts create mode 100644 test/esm/integration/connection/test-insert-large-blob.test.mts create mode 100644 test/esm/integration/connection/test-insert-negative-ai.test.mts create mode 100644 test/esm/integration/connection/test-insert-results.test.mts create mode 100644 test/esm/integration/connection/test-invalid-date-result.test.mts create mode 100644 test/esm/integration/connection/test-load-infile.test.mts create mode 100644 test/esm/integration/connection/test-multiple-results.test.mts create mode 100644 test/esm/integration/connection/test-named-placeholders.test.mts create mode 100644 test/esm/integration/connection/test-nested-tables-query.test.mts create mode 100644 test/esm/integration/connection/test-null-buffer.test.mts create mode 100644 test/esm/integration/connection/test-null-double.test.mts create mode 100644 test/esm/integration/connection/test-null-int.test.mts create mode 100644 test/esm/integration/connection/test-null.test.mts create mode 100644 test/esm/integration/connection/test-parameters-questionmark.test.mts create mode 100644 test/esm/integration/connection/test-prepare-and-close.test.mts create mode 100644 test/esm/integration/connection/test-prepare-simple.test.mts create mode 100644 test/esm/integration/connection/test-prepare-then-execute.test.mts create mode 100644 test/esm/integration/connection/test-protocol-errors.test.mts create mode 100644 test/esm/integration/connection/test-query-timeout.test.mts create mode 100644 test/esm/integration/connection/test-query-zero.test.mts create mode 100644 test/esm/integration/connection/test-quit.test.mts create mode 100644 test/esm/integration/connection/test-select-1.test.mts create mode 100644 test/esm/integration/connection/test-select-empty-string.test.mts create mode 100644 test/esm/integration/connection/test-select-json.test.mts create mode 100644 test/esm/integration/connection/test-select-negative.test.mts create mode 100644 test/esm/integration/connection/test-select-ssl.test.mts create mode 100644 test/esm/integration/connection/test-select-utf8.test.mts create mode 100644 test/esm/integration/connection/test-server-listen.test.mts create mode 100644 test/esm/integration/connection/test-signed-tinyint.test.mts create mode 100644 test/esm/integration/connection/test-stream-error-destroy-connection.test.mts create mode 100644 test/esm/integration/connection/test-stream-errors.test.mts create mode 100644 test/esm/integration/connection/test-stream.test.mts create mode 100644 test/esm/integration/connection/test-then-on-query.test.mts create mode 100644 test/esm/integration/connection/test-timestamp.test.mts create mode 100644 test/esm/integration/connection/test-track-state-change.test.mts create mode 100644 test/esm/integration/connection/test-transaction-commit.test.mts create mode 100644 test/esm/integration/connection/test-transaction-rollback.test.mts create mode 100644 test/esm/integration/connection/test-type-cast-null-fields-execute.test.mts create mode 100644 test/esm/integration/connection/test-type-cast-null-fields.test.mts create mode 100644 test/esm/integration/connection/test-type-casting-execute.test.mts create mode 100644 test/esm/integration/connection/test-type-casting.test.mts create mode 100644 test/esm/integration/connection/test-typecast-execute.test.mts create mode 100644 test/esm/integration/connection/test-typecast-geometry-execute.test.mts create mode 100644 test/esm/integration/connection/test-typecast-geometry.test.mts create mode 100644 test/esm/integration/connection/test-typecast-overwriting-execute.test.mts create mode 100644 test/esm/integration/connection/test-typecast-overwriting.test.mts create mode 100644 test/esm/integration/connection/test-typecast.test.mts create mode 100644 test/esm/integration/connection/test-update-changed-rows.test.mts create mode 100644 test/esm/integration/connection/type-casting-tests.test.mts create mode 100644 test/esm/integration/graceful-end/test-end-with-default-config.test.mts create mode 100644 test/esm/integration/graceful-end/test-end-with-graceful-end-config.test.mts create mode 100644 test/esm/integration/graceful-end/test-pool-release-idle-with-default-config.test.mts create mode 100644 test/esm/integration/graceful-end/test-pool-release-idle-with-graceful-end-config.test.mts create mode 100644 test/esm/integration/promise-wrappers/test-async-stack.test.mts create mode 100644 test/esm/integration/promise-wrappers/test-promise-wrappers.test.mts create mode 100644 test/esm/integration/regressions/test-#433.test.mts create mode 100644 test/esm/integration/regressions/test-#442.test.mts create mode 100644 test/esm/integration/regressions/test-#485.test.mts create mode 100644 test/esm/integration/regressions/test-#617.test.mts create mode 100644 test/esm/integration/regressions/test-#629.test.mts create mode 100644 test/esm/integration/regressions/test-#82.test.mts create mode 100644 test/esm/integration/test-auth-switch-multi-factor.test.mts create mode 100644 test/esm/integration/test-auth-switch-plugin-async-error.test.mts create mode 100644 test/esm/integration/test-auth-switch-plugin-error.test.mts create mode 100644 test/esm/integration/test-auth-switch.test.mts create mode 100644 test/esm/integration/test-handshake-unknown-packet-error.test.mts create mode 100644 test/esm/integration/test-multi-result-streaming.test.mts create mode 100644 test/esm/integration/test-pool-connect-error.test.mts create mode 100644 test/esm/integration/test-pool-disconnect.test.mts create mode 100644 test/esm/integration/test-pool-end.test.mts create mode 100644 test/esm/integration/test-pool-release-idle-connection-replicate.test.mts create mode 100644 test/esm/integration/test-pool-release-idle-connection-timeout.test.mts create mode 100644 test/esm/integration/test-pool-release-idle-connection.test.mts create mode 100644 test/esm/integration/test-pool-release.test.mts create mode 100644 test/esm/integration/test-rows-as-array.test.mts create mode 100644 test/esm/integration/test-server-close.test.mts create mode 100644 test/esm/unit/commands/test-query.test.mts create mode 100644 test/esm/unit/commands/test-quit.test.mts create mode 100644 test/esm/unit/connection/test-connection-state.test.mts create mode 100644 test/esm/unit/connection/test-connection_config.test.mts create mode 100644 test/esm/unit/packets/test-column-definition.test.mts create mode 100644 test/esm/unit/packets/test-datetime.test.mts create mode 100644 test/esm/unit/packets/test-ok-autoinc.test.mts create mode 100644 test/esm/unit/packets/test-ok-sessiontrack.test.mts create mode 100644 test/esm/unit/packets/test-text-row.test.mts create mode 100644 test/esm/unit/packets/test-time.test.mts create mode 100644 test/esm/unit/parsers/test-text-parser.test.mts create mode 100644 test/esm/unit/pool-cluster/test-connection-error-remove.test.mts create mode 100644 test/esm/unit/pool-cluster/test-connection-order.test.mts create mode 100644 test/esm/unit/pool-cluster/test-connection-retry.test.mts create mode 100644 test/esm/unit/pool-cluster/test-connection-rr.test.mts create mode 100644 test/esm/unit/pool-cluster/test-query.test.mts create mode 100644 test/esm/unit/pool-cluster/test-remove-by-name.test.mts create mode 100644 test/esm/unit/pool-cluster/test-remove-by-pattern.test.mts create mode 100644 test/esm/unit/pool-cluster/test-restore-events.test.mts create mode 100644 test/esm/unit/pool-cluster/test-restore.test.mts create mode 100644 test/esm/unit/test-packet-parser.test.mts create mode 100644 test/globals.d.ts diff --git a/.prettierrc b/.prettierrc index 3d25d8fc93..36b0bf66e5 100644 --- a/.prettierrc +++ b/.prettierrc @@ -13,5 +13,27 @@ "htmlWhitespaceSensitivity": "css", "endOfLine": "lf", "embeddedLanguageFormatting": "auto", - "singleAttributePerLine": false + "singleAttributePerLine": false, + "overrides": [ + { + "files": "*.jsonc", + "options": { + "trailingComma": "none" + } + }, + { + "files": "test/esm/**/*.mts", + "options": { + "plugins": ["@ianvs/prettier-plugin-sort-imports"], + "importOrder": [ + "^(node:)", + "", + "^[.]", + "", + "", + "^[.]" + ] + } + } + ] } diff --git a/eslint.config.mjs b/eslint.config.mjs index 8f3bed2b70..41acda3d44 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -97,15 +97,15 @@ export default [ }, rules: { '@typescript-eslint/no-empty-interface': 'off', - '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-empty-object-type': 'off', strict: 'off', 'no-restricted-syntax': [ 'error', { selector: - 'ImportDeclaration[source.value=/^\\./][source.value!=/\\.(m?js)$/]', - message: 'Local imports must have the explicit .mjs or .js extension', + 'ImportDeclaration[source.value=/^\\./][source.value!=/\\.(m?js|json)$/]', + message: + 'Local imports must have the explicit .mjs, .js, or .json extension', }, ], }, @@ -144,6 +144,13 @@ export default [ 'arrow-parens': ['error', 'always'], }, }, + { + files: ['test/esm/**/*.mts'], + rules: { + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/no-unsafe-function-type': 'off', + }, + }, { files: ['**/*.mjs', '**/*.mts'], languageOptions: { diff --git a/package-lock.json b/package-lock.json index 23ef10b047..e027f7c5e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@eslint/eslintrc": "^3.3.3", "@eslint/js": "^9.39.2", "@eslint/markdown": "^7.5.1", + "@ianvs/prettier-plugin-sort-imports": "^4.7.1", "@types/node": "^25.0.9", "@typescript-eslint/eslint-plugin": "^8.53.0", "@typescript-eslint/parser": "^8.53.0", @@ -35,7 +36,7 @@ "eslint-plugin-markdown": "^5.1.0", "eslint-plugin-prettier": "^5.5.5", "globals": "^17.0.0", - "poku": "^3.0.2", + "poku": "^3.0.3-canary.8f374795", "portfinder": "^1.0.38", "prettier": "^3.8.0", "tsx": "^4.21.0", @@ -45,6 +46,132 @@ "node": ">= 8.0" } }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@bcoe/v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", @@ -527,33 +654,120 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.1.tgz", + "integrity": "sha512-uVSdg/V4dfQmTjJzR0szNczjOH/J+FyUMMjYtr07xFRXR7EDf9i1qdxrD0VusZH9knj1/ecxzCQQxyic5NzAiA==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { - "@eslint/object-schema": "^2.1.7", + "@eslint/object-schema": "^3.0.1", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^10.1.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-array/node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.2.tgz", + "integrity": "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "jackspeak": "^4.2.3" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", + "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@eslint/config-array/node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.1.tgz", + "integrity": "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==", + "dev": true, + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.2.tgz", + "integrity": "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { - "@eslint/core": "^0.17.0" + "@eslint/core": "^1.1.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers/node_modules/@eslint/core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz", + "integrity": "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { @@ -644,14 +858,14 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.1.tgz", + "integrity": "sha512-P9cq2dpr+LU8j3qbLygLcSZrl2/ds/pUpfnHNNuk5HW7mnngHs+6WSq5C9mO3rqRX8A1poxqLTC9cu0KOyJlBg==", "dev": true, "license": "Apache-2.0", "peer": true, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/plugin-kit": { @@ -733,6 +947,41 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@ianvs/prettier-plugin-sort-imports": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/@ianvs/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.7.1.tgz", + "integrity": "sha512-jmTNYGlg95tlsoG3JLCcuC4BrFELJtLirLAkQW/71lXSyOhVt/Xj7xWbbGcuVbNq1gwWgSyMrPjJc9Z30hynVw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/generator": "^7.26.2", + "@babel/parser": "^7.26.2", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.26.0", + "semver": "^7.5.2" + }, + "peerDependencies": { + "@prettier/plugin-oxc": "^0.0.4 || ^0.1.0", + "@vue/compiler-sfc": "2.7.x || 3.x", + "content-tag": "^4.0.0", + "prettier": "2 || 3 || ^4.0.0-0", + "prettier-plugin-ember-template-tag": "^2.1.0" + }, + "peerDependenciesMeta": { + "@prettier/plugin-oxc": { + "optional": true + }, + "@vue/compiler-sfc": { + "optional": true + }, + "content-tag": { + "optional": true + }, + "prettier-plugin-ember-template-tag": { + "optional": true + } + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -761,6 +1010,17 @@ "node": ">=8" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -823,6 +1083,14 @@ "@types/ms": "*" } }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -880,17 +1148,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.55.0.tgz", - "integrity": "sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", + "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.55.0", - "@typescript-eslint/type-utils": "8.55.0", - "@typescript-eslint/utils": "8.55.0", - "@typescript-eslint/visitor-keys": "8.55.0", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/type-utils": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -903,8 +1171,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.55.0", - "eslint": "^8.57.0 || ^9.0.0", + "@typescript-eslint/parser": "^8.56.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, @@ -919,16 +1187,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.55.0.tgz", - "integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", + "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.55.0", - "@typescript-eslint/types": "8.55.0", - "@typescript-eslint/typescript-estree": "8.55.0", - "@typescript-eslint/visitor-keys": "8.55.0", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", "debug": "^4.4.3" }, "engines": { @@ -939,19 +1207,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.55.0.tgz", - "integrity": "sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", + "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.55.0", - "@typescript-eslint/types": "^8.55.0", + "@typescript-eslint/tsconfig-utils": "^8.56.0", + "@typescript-eslint/types": "^8.56.0", "debug": "^4.4.3" }, "engines": { @@ -966,14 +1234,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.55.0.tgz", - "integrity": "sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", + "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.55.0", - "@typescript-eslint/visitor-keys": "8.55.0" + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -984,9 +1252,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.55.0.tgz", - "integrity": "sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", + "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", "dev": true, "license": "MIT", "engines": { @@ -1001,15 +1269,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.55.0.tgz", - "integrity": "sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz", + "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.55.0", - "@typescript-eslint/typescript-estree": "8.55.0", - "@typescript-eslint/utils": "8.55.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -1021,14 +1289,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.55.0.tgz", - "integrity": "sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", + "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", "dev": true, "license": "MIT", "engines": { @@ -1040,16 +1308,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.55.0.tgz", - "integrity": "sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", + "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.55.0", - "@typescript-eslint/tsconfig-utils": "8.55.0", - "@typescript-eslint/types": "8.55.0", - "@typescript-eslint/visitor-keys": "8.55.0", + "@typescript-eslint/project-service": "8.56.0", + "@typescript-eslint/tsconfig-utils": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", @@ -1094,16 +1362,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.55.0.tgz", - "integrity": "sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", + "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.55.0", - "@typescript-eslint/types": "8.55.0", - "@typescript-eslint/typescript-estree": "8.55.0" + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1113,19 +1381,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.55.0.tgz", - "integrity": "sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", + "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.55.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.56.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1136,13 +1404,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", + "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -1345,24 +1613,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/character-entities": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", @@ -1709,34 +1959,31 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.0.tgz", + "integrity": "sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", - "@eslint/plugin-kit": "^0.4.1", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.0", + "@eslint/config-helpers": "^0.5.2", + "@eslint/core": "^1.1.0", + "@eslint/plugin-kit": "^0.6.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", - "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", + "eslint-scope": "^9.1.0", + "eslint-visitor-keys": "^5.0.0", + "espree": "^11.1.0", + "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", @@ -1746,8 +1993,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^10.1.1", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -1755,7 +2001,7 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://eslint.org/donate" @@ -1922,18 +2168,20 @@ } }, "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.0.tgz", + "integrity": "sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ==", "dev": true, "license": "BSD-2-Clause", "peer": true, "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -1952,20 +2200,141 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz", + "integrity": "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/eslint/node_modules/@eslint/plugin-kit": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.0.tgz", + "integrity": "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@eslint/core": "^1.1.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/eslint/node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.2.tgz", + "integrity": "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "jackspeak": "^4.2.3" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", + "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", + "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", "dev": true, "license": "Apache-2.0", "peer": true, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/espree": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.0.tgz", + "integrity": "sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.1.tgz", + "integrity": "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==", + "dev": true, + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -2546,6 +2915,13 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -2559,6 +2935,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -2648,14 +3037,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -3762,6 +4143,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, "node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", @@ -3783,9 +4171,9 @@ "license": "MIT" }, "node_modules/poku": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/poku/-/poku-3.0.2.tgz", - "integrity": "sha512-vwNcV5Y+gEhq1Sxw34HTZ4rTudg7LgaEoyZ4m7CXH/k9JKTTwwnj9fZGrstbEBR2e2TCroNHqt6cVGBAg6zkAA==", + "version": "3.0.3-canary.8f374795", + "resolved": "https://registry.npmjs.org/poku/-/poku-3.0.3-canary.8f374795.tgz", + "integrity": "sha512-IjcOXmnFrDr6YblPFxm+QK1C6bdy6DIywcCHANUQW6X5XOMR6ha7M9ZwQRA726DsAaVkVCqTNzl5o1benK+zIw==", "dev": true, "license": "MIT", "bin": { @@ -3794,7 +4182,7 @@ "engines": { "bun": ">=1.x.x", "deno": ">=1.x.x", - "node": ">=14.x.x", + "node": ">=16.x.x", "typescript": ">=5.x.x" }, "funding": { diff --git a/package.json b/package.json index 45a9cb5039..837637c6c3 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,9 @@ "scripts": { "lint": "eslint . && prettier --check .", "lint:fix": "eslint . --fix && prettier --write .", - "test": "poku -d -r=verbose --sequential test/esm test/unit test/integration", - "test:bun": "bun poku -d --sequential test/esm test/unit test/integration", - "test:deno": "deno run --allow-read --allow-env --allow-run npm:poku -d --sequential --denoAllow=\"read,env,net,sys\" test/esm test/unit test/integration", + "test": "poku -d -r=verbose --sequential test/esm", + "test:bun": "bun poku -d --sequential test/esm", + "test:deno": "deno run --allow-read --allow-env --allow-run npm:poku -d --sequential --denoAllow=\"read,env,net,sys\" test/esm", "test:docker:up": "docker compose -f test/docker-compose.yml up --abort-on-container-exit --remove-orphans", "test:docker:down": "docker compose -f test/docker-compose.yml down", "test:docker:node": "npm run test:docker:up -- node && npm run test:docker:down", @@ -66,6 +66,7 @@ "@eslint/eslintrc": "^3.3.3", "@eslint/js": "^9.39.2", "@eslint/markdown": "^7.5.1", + "@ianvs/prettier-plugin-sort-imports": "^4.7.1", "@types/node": "^25.0.9", "@typescript-eslint/eslint-plugin": "^8.53.0", "@typescript-eslint/parser": "^8.53.0", @@ -78,7 +79,7 @@ "eslint-plugin-markdown": "^5.1.0", "eslint-plugin-prettier": "^5.5.5", "globals": "^17.0.0", - "poku": "^3.0.2", + "poku": "^3.0.3-canary.8f374795", "portfinder": "^1.0.38", "prettier": "^3.8.0", "tsx": "^4.21.0", diff --git a/test/docker-compose.yml b/test/docker-compose.yml index 42744f8f66..388af2c699 100644 --- a/test/docker-compose.yml +++ b/test/docker-compose.yml @@ -6,6 +6,8 @@ services: environment: MYSQL_DATABASE: 'test' MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' + ports: + - '3306:3306' healthcheck: test: ['CMD', 'mysql', '-h', 'localhost', '-u', 'root', '-e', 'SELECT 1'] interval: 1s diff --git a/test/esm/common.test.mts b/test/esm/common.test.mts index a8e6560465..e794973ec5 100644 --- a/test/esm/common.test.mts +++ b/test/esm/common.test.mts @@ -1,19 +1,23 @@ -import type { Connection as PromiseConnection } from '../../promise.js'; import type { + Connection, ConnectionOptions, - PoolOptions, PoolClusterOptions, - Connection, + PoolOptions, } from '../../index.js'; -import { createRequire } from 'node:module'; +import type { + Connection as PromiseConnection, + RowDataPacket, +} from '../../promise.js'; import fs from 'node:fs'; +import { createRequire } from 'node:module'; import path from 'node:path'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; -export * as SqlString from 'sql-escaper'; import portfinder from 'portfinder'; +import driver from '../../index.js'; import ClientFlags from '../../lib/constants/client.js'; -import * as driver from '../../index.js'; + +export * as SqlString from 'sql-escaper'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -182,7 +186,9 @@ export const getConfig = function (input?: ConnectionOptions) { return params; }; -export const createPool = function (args?: PoolOptions) { +export const createPool = function ( + args?: PoolOptions +): ReturnType { if (!args?.port && process.env.MYSQL_CONNECTION_URL) { return driver.createPool({ ...args, @@ -202,7 +208,9 @@ export const createPool = function (args?: PoolOptions) { return driver.createPool(getConfig(args)); }; -export const createPoolCluster = function (args: PoolClusterOptions = {}) { +export const createPoolCluster = function ( + args: PoolClusterOptions = {} +): ReturnType { if (!('port' in args) && process.env.MYSQL_CONNECTION_URL) { return driver.createPoolCluster({ ...args, @@ -214,7 +222,9 @@ export const createPoolCluster = function (args: PoolClusterOptions = {}) { return driver.createPoolCluster(args); }; -export const createConnectionWithURI = function () { +export const createConnectionWithURI = function (): ReturnType< + typeof driver.createConnection +> { return driver.createConnection({ uri: configURI }); }; @@ -273,7 +283,9 @@ export const getMysqlVersion = async function ( connection: Connection | PromiseConnection ) { const conn = 'promise' in connection ? connection.promise() : connection; - const [rows] = await conn.query('SELECT VERSION() AS `version`'); + const [rows] = await conn.query<(RowDataPacket & { version: string })[]>( + 'SELECT VERSION() AS `version`' + ); const serverVersion: string = rows[0].version; const [major, minor, patch] = serverVersion diff --git a/test/esm/integration/config/test-connect-timeout.test.mts b/test/esm/integration/config/test-connect-timeout.test.mts new file mode 100644 index 0000000000..57528d6cf9 --- /dev/null +++ b/test/esm/integration/config/test-connect-timeout.test.mts @@ -0,0 +1,41 @@ +import assert from 'node:assert'; +import process from 'node:process'; +import portfinder from 'portfinder'; +import mysql from '../../../../index.js'; + +// The process is not terminated in Deno +if (typeof Deno !== 'undefined') process.exit(0); + +console.log('test connect timeout'); + +portfinder.getPort((_, port) => { + // @ts-expect-error: TODO: implement typings + const server = mysql.createServer(); + server.on('connection', () => { + // Let connection time out + }); + + server.listen(port); + + const connection = mysql.createConnection({ + host: 'localhost', + port: port, + connectTimeout: 1000, + }); + + connection.on('error', (err) => { + assert.equal(err.code, 'ETIMEDOUT'); + connection.destroy(); + // @ts-expect-error: internal access + server._server.close(); + console.log('ok'); + }); +}); + +process.on('uncaughtException', (err: NodeJS.ErrnoException) => { + assert.equal( + err.message, + 'Connection lost: The server closed the connection.' + ); + assert.equal(err.code, 'PROTOCOL_CONNECTION_LOST'); +}); diff --git a/test/esm/integration/config/test-typecast-global-false.test.mts b/test/esm/integration/config/test-typecast-global-false.test.mts new file mode 100644 index 0000000000..87a3ad0401 --- /dev/null +++ b/test/esm/integration/config/test-typecast-global-false.test.mts @@ -0,0 +1,35 @@ +import type { RowDataPacket } from '../../../../index.js'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection({ + typeCast: false, +}); + +const COL_1_VALUE = 'col v1'; +const COL_2_VALUE = 'col v2'; + +function executeTests(res: RowDataPacket[]) { + assert.equal(res[0].v1.toString('ascii'), COL_1_VALUE); + assert.equal(res[0].n1, null); + assert.equal(res[0].v2.toString('ascii'), COL_2_VALUE); +} + +connection.query( + 'CREATE TEMPORARY TABLE binpar_null_test (v1 VARCHAR(16) NOT NULL, n1 VARCHAR(16), v2 VARCHAR(16) NOT NULL)' +); +connection.query( + `INSERT INTO binpar_null_test (v1, n1, v2) VALUES ("${COL_1_VALUE}", NULL, "${COL_2_VALUE}")`, + (err) => { + if (err) throw err; + } +); + +connection.execute( + 'SELECT * FROM binpar_null_test', + (err, res: RowDataPacket[]) => { + if (err) throw err; + executeTests(res); + connection.end(); + } +); diff --git a/test/esm/integration/config/test-typecast-global-option.test.mts b/test/esm/integration/config/test-typecast-global-option.test.mts new file mode 100644 index 0000000000..84ffbf1402 --- /dev/null +++ b/test/esm/integration/config/test-typecast-global-option.test.mts @@ -0,0 +1,48 @@ +import type { + RowDataPacket, + TypeCastField, + TypeCastNext, +} from '../../../../index.js'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +type StringMethod = 'toUpperCase' | 'toLowerCase'; + +const typeCastWrapper = function (stringMethod: StringMethod) { + return function (field: TypeCastField, next: TypeCastNext) { + if (field.type === 'VAR_STRING') { + const value = field.string(); + return value?.[stringMethod](); + } + return next(); + }; +}; + +const connection = createConnection({ + typeCast: typeCastWrapper('toUpperCase'), +}); + +// query option override global typeCast +connection.query( + { + sql: 'select "FOOBAR" as foo', + typeCast: typeCastWrapper('toLowerCase'), + }, + (err, res) => { + assert.ifError(err); + assert.equal(res[0].foo, 'foobar'); + } +); + +// global typecast works +connection.query( + { + sql: 'select "foobar" as foo', + }, + (err, res) => { + assert.ifError(err); + assert.equal(res[0].foo, 'FOOBAR'); + } +); + +connection.end(); diff --git a/test/esm/integration/connection/encoding/test-charset-results.test.mts b/test/esm/integration/connection/encoding/test-charset-results.test.mts new file mode 100644 index 0000000000..b607e7f809 --- /dev/null +++ b/test/esm/integration/connection/encoding/test-charset-results.test.mts @@ -0,0 +1,93 @@ +import type { RowDataPacket } from '../../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import mysql from '../../../../../index.js'; +import { createConnection } from '../../../common.test.mjs'; + +type PayloadRow = RowDataPacket & Record; + +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test for planetscale (unsupported non utf8 charsets)'); + process.exit(0); +} + +const connection = createConnection(); + +const payload = 'привет, мир'; +function tryEncoding(encoding: string, cb: () => void) { + connection.query('set character_set_results = ?', [encoding], (err) => { + assert.ifError(err); + connection.query( + 'SELECT ?', + [payload], + (err, rows, fields) => { + assert.ifError(err); + if (!fields || fields.length === 0) { + assert.fail('Expected metadata fields'); + } + const firstField = fields[0]; + const characterSet = firstField.characterSet; + if (characterSet === undefined) { + assert.fail('Expected characterSet metadata'); + } + let iconvEncoding = encoding; + if (encoding === 'utf8mb4') { + iconvEncoding = 'utf8'; + } + assert.equal(mysql.CharsetToEncoding[characterSet], iconvEncoding); + assert.equal(firstField.name, payload); + assert.equal(rows[0][firstField.name], payload); + cb(); + } + ); + }); +} + +function tryEncodingExecute(encoding: string, cb: () => void) { + connection.execute('set character_set_results = ?', [encoding], (err) => { + assert.ifError(err); + connection.execute( + 'SELECT ? as n', + [payload], + (err, rows, fields) => { + assert.ifError(err); + if (!fields || fields.length === 0) { + assert.fail('Expected metadata fields'); + } + const firstField = fields[0]; + const characterSet = firstField.characterSet; + if (characterSet === undefined) { + assert.fail('Expected characterSet metadata'); + } + let iconvEncoding = encoding; + if (encoding === 'utf8mb4') { + iconvEncoding = 'utf8'; + } + assert.equal(mysql.CharsetToEncoding[characterSet], iconvEncoding); + // TODO: figure out correct metadata encodings setup for binary protocol + // assert.equal(firstField.name, payload); + assert.equal(rows[0][firstField.name], payload); + cb(); + } + ); + }); +} + +// christmas tree!!! :) +tryEncoding('cp1251', () => { + tryEncoding('koi8r', () => { + tryEncoding('cp866', () => { + tryEncoding('utf8mb4', () => { + tryEncodingExecute('cp1251', () => { + tryEncodingExecute('koi8r', () => { + tryEncodingExecute('cp866', () => { + tryEncodingExecute('utf8mb4', () => { + connection.end(); + }); + }); + }); + }); + }); + }); + }); +}); diff --git a/test/esm/integration/connection/encoding/test-client-encodings.test.mts b/test/esm/integration/connection/encoding/test-client-encodings.test.mts new file mode 100644 index 0000000000..545ca29c0f --- /dev/null +++ b/test/esm/integration/connection/encoding/test-client-encodings.test.mts @@ -0,0 +1,45 @@ +import type { RowDataPacket } from '../../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../../common.test.mjs'; + +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test for planetscale (unsupported non utf8 charsets)'); + process.exit(0); +} + +const connection = createConnection({ charset: 'UTF8MB4_GENERAL_CI' }); +connection.query('drop table if exists __test_client_encodings'); +connection.query( + 'create table if not exists __test_client_encodings (name VARCHAR(200)) CHARACTER SET=utf8mb4', + (err) => { + assert.ifError(err); + connection.query('delete from __test_client_encodings', (err) => { + assert.ifError(err); + connection.end(); + + const connection1 = createConnection({ + charset: 'CP1251_GENERAL_CI', + }); + connection1.query( + 'insert into __test_client_encodings values("привет, мир")', + (err) => { + assert.ifError(err); + connection1.end(); + + const connection2 = createConnection({ + charset: 'KOI8R_GENERAL_CI', + }); + connection2.query( + 'select * from __test_client_encodings', + (err, rows) => { + assert.ifError(err); + assert.equal(rows[0].name, 'привет, мир'); + connection2.end(); + } + ); + } + ); + }); + } +); diff --git a/test/esm/integration/connection/encoding/test-non-bmp-chars.test.mts b/test/esm/integration/connection/encoding/test-non-bmp-chars.test.mts new file mode 100644 index 0000000000..00990bcbc3 --- /dev/null +++ b/test/esm/integration/connection/encoding/test-non-bmp-chars.test.mts @@ -0,0 +1,36 @@ +import type { FieldPacket, RowDataPacket } from '../../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../../common.test.mjs'; + +type UtfRow = RowDataPacket & Record; + +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test for planetscale'); + process.exit(0); +} + +// 4 bytes in utf8 +const pileOfPoo = '💩'; + +const connection = createConnection({ charset: 'UTF8_GENERAL_CI' }); +connection.query( + 'select "💩"', + (err, rows, fields: FieldPacket[]) => { + assert.ifError(err); + assert.equal(fields[0].name, pileOfPoo); + assert.equal(rows[0][fields[0].name], pileOfPoo); + connection.end(); + } +); + +const connection2 = createConnection({ charset: 'UTF8MB4_GENERAL_CI' }); +connection2.query( + 'select "💩"', + (err, rows, fields: FieldPacket[]) => { + assert.ifError(err); + assert.equal(fields[0].name, '?'); + assert.equal(rows[0]['?'], pileOfPoo); + connection2.end(); + } +); diff --git a/test/esm/integration/connection/encoding/test-track-encodings.test.mts b/test/esm/integration/connection/encoding/test-track-encodings.test.mts new file mode 100644 index 0000000000..552282c1bb --- /dev/null +++ b/test/esm/integration/connection/encoding/test-track-encodings.test.mts @@ -0,0 +1,30 @@ +import type { RowDataPacket } from '../../../../../index.js'; +import { assert } from 'poku'; +import { createConnection } from '../../../common.test.mjs'; + +const connection = createConnection({ charset: 'UTF8MB4_GENERAL_CI' }); +const text = 'привет, мир'; + +connection.query('SET character_set_client=koi8r', (err) => { + assert.ifError(err); + connection.query( + `SELECT ? as result`, + [text], + (err, rows) => { + assert.ifError(err); + assert.equal(rows[0].result, text); + connection.query('SET character_set_client=cp1251', (err) => { + assert.ifError(err); + connection.query( + `SELECT ? as result`, + [text], + (err, rows) => { + assert.ifError(err); + assert.equal(rows[0].result, text); + connection.end(); + } + ); + }); + } + ); +}); diff --git a/test/esm/integration/connection/test-aurora-failover-readonly.test.mts b/test/esm/integration/connection/test-aurora-failover-readonly.test.mts new file mode 100644 index 0000000000..fa15307d13 --- /dev/null +++ b/test/esm/integration/connection/test-aurora-failover-readonly.test.mts @@ -0,0 +1,181 @@ +import type { QueryError } from '../../../../index.js'; +import process from 'node:process'; +import { assert, describe, it } from 'poku'; +import portfinder from 'portfinder'; +import mysql from '../../../../index.js'; +import ClientFlags from '../../../../lib/constants/client.js'; + +type FailoverResult = { + errorReceived: QueryError | undefined; + firstThreadId: number; + secondThreadId: number; +}; + +// The process is not terminated in Deno +if (typeof Deno !== 'undefined') process.exit(0); + +// Simulate Aurora MySQL failover: a writable connection becomes read-only. +// After failover, writes return one of these read-only errors: +// 1290: ER_OPTION_PREVENTS_STATEMENT (server running with --read-only) +// 1792: ER_CANT_EXECUTE_IN_READ_ONLY_TRANSACTION +// 1836: ER_READ_ONLY_MODE +// +// This test verifies that when a pooled connection hits any of these errors, +// the pool discards that connection and creates a fresh one for the next +// caller, rather than returning the stale read-only connection. + +let connId = 0; + +const readOnlyErrors = [ + { + code: 1290, + name: 'ER_OPTION_PREVENTS_STATEMENT', + message: + 'The MySQL server is running with the --read-only option so it cannot execute this statement', + }, + { + code: 1792, + name: 'ER_CANT_EXECUTE_IN_READ_ONLY_TRANSACTION', + message: 'Cannot execute statement in a READ ONLY transaction', + }, + { + code: 1836, + name: 'ER_READ_ONLY_MODE', + message: 'Running in read-only mode', + }, +]; + +// Helper to create a test scenario for a specific error code and method +async function testReadOnlyError( + errorConfig: (typeof readOnlyErrors)[number], + method: string +) { + return new Promise((resolve, reject) => { + let failoverTriggered = false; + + const server = mysql.createServer((conn) => { + conn.on('error', () => {}); + + let flags = 0xffffff; + flags = flags ^ (ClientFlags.COMPRESS | ClientFlags.SSL); + + conn.serverHandshake({ + protocolVersion: 10, + serverVersion: 'node.js rocks', + connectionId: ++connId, + statusFlags: 2, + characterSet: 8, + capabilityFlags: flags, + }); + + conn.on('query', () => { + if (!failoverTriggered) { + conn.writeOk(); + } else { + conn.writeError(errorConfig); + } + }); + + conn.on('stmt_prepare', () => { + if (!failoverTriggered) { + conn.writeOk(); + } else { + conn.writeError(errorConfig); + } + }); + }); + + portfinder.getPort(async (_err, port) => { + server.listen(port); + + const pool = mysql + .createPool({ + host: 'localhost', + port: port, + user: 'test', + database: 'test', + connectionLimit: 1, + }) + .promise(); + + try { + // First query succeeds — connection is writable + await pool.query('INSERT INTO t VALUES (1)'); + + // Get initial connection threadId + const conn = await pool.getConnection(); + const firstThreadId = conn.threadId; + conn.release(); + + // Trigger failover — all subsequent writes return read-only error + failoverTriggered = true; + + // Execute test based on method type + let errorReceived: QueryError | undefined; + try { + if (method === 'query') { + await pool.query('INSERT INTO t VALUES (2)'); + } else { + await pool.execute('INSERT INTO t VALUES (?)', [2]); + } + } catch (err) { + errorReceived = err as QueryError; + } + + // After error, pool should have discarded connection and created new one + const conn2 = await pool.getConnection(); + const secondThreadId = conn2.threadId; + conn2.release(); + + // Cleanup + await pool.end(); + + server.close(() => { + resolve({ + errorReceived, + firstThreadId, + secondThreadId, + }); + }); + } catch (err) { + reject(err); + } + }); + }); +} + +describe('Aurora MySQL Failover - Read-Only Error Handling', async () => { + for (const errorConfig of readOnlyErrors) { + await it(`should discard connection on pool.query() when error ${errorConfig.code} occurs`, async () => { + const result = await testReadOnlyError(errorConfig, 'query'); + + assert.ok(result.errorReceived, 'Should receive an error'); + assert.equal( + result.errorReceived?.errno, + errorConfig.code, + `Error code should be ${errorConfig.code}` + ); + assert.notEqual( + result.firstThreadId, + result.secondThreadId, + 'Pool should have created a new connection (different threadId)' + ); + }); + + await it(`should discard connection on pool.execute() when error ${errorConfig.code} occurs`, async () => { + const result = await testReadOnlyError(errorConfig, 'execute'); + + assert.ok(result.errorReceived, 'Should receive an error'); + assert.equal( + result.errorReceived?.errno, + errorConfig.code, + `Error code should be ${errorConfig.code}` + ); + assert.notEqual( + result.firstThreadId, + result.secondThreadId, + 'Pool should have created a new connection (different threadId)' + ); + }); + } +}); diff --git a/test/esm/integration/connection/test-backpressure-load-data-infile.test.mts b/test/esm/integration/connection/test-backpressure-load-data-infile.test.mts new file mode 100644 index 0000000000..44d01397c5 --- /dev/null +++ b/test/esm/integration/connection/test-backpressure-load-data-infile.test.mts @@ -0,0 +1,110 @@ +import type { QueryError, ResultSetHeader } from '../../../../index.js'; +import Net from 'node:net'; +import { Duplex, Readable } from 'node:stream'; +import { assert, log, skip, sleep, test } from 'poku'; +import driver from '../../../../index.js'; +import { config } from '../../common.test.mjs'; + +if (config.compress) { + skip( + 'skipping test with compression; load data infile backpressure is not working with compression' + ); +} + +class BigInput extends Readable { + count = 0; + MAX_EXPECTED_ROWS = 100_000; + onStart: (() => void) | null = null; + + _read() { + if (this.onStart) { + this.onStart(); + this.onStart = null; + } + + if (this.count < this.MAX_EXPECTED_ROWS) { + this.count++; + const row = `${this.count}\n`; + this.push(row); + } else { + this.push(null); + } + } +} + +test('load data infile backpressure on local stream', async () => { + const netStream = Net.connect(config.port, config.host); + netStream.setNoDelay(true); + await new Promise((resolve, reject) => + netStream.once('connect', resolve).once('error', reject) + ); + + class NetworkInterceptor extends Duplex { + simulateWriteBackpressure = false; + + constructor() { + super({ writableHighWaterMark: 65536 }); + netStream.on('data', (data: Buffer) => { + const continueReading = this.push(data); + if (!continueReading) { + netStream.pause(); + } + }); + netStream.on('error', (err: Error) => this.destroy(err)); + } + + _read() { + netStream.resume(); + } + + _write( + chunk: Buffer, + encoding: BufferEncoding, + callback: (err?: Error | null) => void + ) { + netStream.write(chunk, encoding, (err?: Error | null) => { + if (err) { + callback(err); + } else if (!this.simulateWriteBackpressure) { + callback(); + } + }); + } + } + + const interceptor = new NetworkInterceptor(); + const connection = driver.createConnection({ + ...config, + multipleStatements: true, + stream: interceptor, + }); + + const bigInput = new BigInput(); + bigInput.onStart = () => (interceptor.simulateWriteBackpressure = true); + + connection.query( + { + sql: ` + set global local_infile = 1; + create temporary table test_load_data_backpressure (id varchar(100)); + load data local infile "_" replace into table test_load_data_backpressure; + `, + infileStreamFactory: () => bigInput, + }, + (err: QueryError | null, result: ResultSetHeader[]) => { + if (err) throw err; + log('Load complete', result); + } + ); + + await sleep(1000); // allow time for backpressure to take effect + + // @ts-expect-error: TODO: implement typings + connection.close(); + netStream.destroy(); + + assert.ok( + bigInput.count < bigInput.MAX_EXPECTED_ROWS, + `expected backpressure to stop infile stream at less than ${bigInput.MAX_EXPECTED_ROWS} rows (read ${bigInput.count} rows)` + ); +}); diff --git a/test/esm/integration/connection/test-backpressure-result-streaming.test.mts b/test/esm/integration/connection/test-backpressure-result-streaming.test.mts new file mode 100644 index 0000000000..f7acdba053 --- /dev/null +++ b/test/esm/integration/connection/test-backpressure-result-streaming.test.mts @@ -0,0 +1,60 @@ +import type { RowDataPacket } from '../../../../index.js'; +import { assert, skip, sleep, test } from 'poku'; +import { createConnection, getMysqlVersion } from '../../common.test.mjs'; + +test('result event backpressure with pause/resume', async () => { + const connection = createConnection({ + multipleStatements: true, + }); + + const mySqlVersion = await getMysqlVersion(connection); + if (mySqlVersion.major < 8) { + skip('MySQL >= 8.0 required to use CTE'); + } + + // in case wrapping with TLS, get the underlying socket first so we can see actual number of bytes read + // @ts-expect-error: TODO: implement typings + const originalSocket: { bytesRead: number } = connection.stream; + + // the full result set will be over 6 MB uncompressed; about 490 KB with compression + const largeQuery = ` + SET SESSION cte_max_recursion_depth = 100000; + WITH RECURSIVE cte (n, s) AS ( + SELECT 1, 'this is just to cause more bytes transferred for each row' + UNION ALL + SELECT n + 1, s + FROM cte + WHERE n < 100000 + ) + SELECT * FROM cte; + `; + + let resultRowsCount = 0; + await new Promise((resolve, reject) => + connection + .query(largeQuery) + .on('result', (row: RowDataPacket) => { + resultRowsCount++; + if (row.n === 1) { + connection.pause(); + resolve(); + } + }) + .on('error', reject) + ); + + // if backpressure is not working, the bytes received will grow during this time, even though connection is paused + await sleep(500); + + assert.equal(resultRowsCount, 2, 'stop receiving result rows when paused'); + + // if backpressure is working, there should be less than 300 KB received; + // experimentally it appears to be around 100 KB but may vary if buffer sizes change + assert.ok( + originalSocket.bytesRead < 300000, + `Received ${originalSocket.bytesRead} bytes on paused connection` + ); + + // @ts-expect-error: TODO: implement typings + connection.close(); +}); diff --git a/test/esm/integration/connection/test-binary-charset-string.test.mts b/test/esm/integration/connection/test-binary-charset-string.test.mts new file mode 100644 index 0000000000..7b8016a98e --- /dev/null +++ b/test/esm/integration/connection/test-binary-charset-string.test.mts @@ -0,0 +1,96 @@ +import type { FieldPacket, RowDataPacket } from '../../../../index.js'; +import { Buffer } from 'node:buffer'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +// TODO - this could be re-enabled +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test for planetscale'); + process.exit(0); +} + +let rows: RowDataPacket[] | undefined; +let fields: FieldPacket[] | undefined; +let rows1: RowDataPacket[] | undefined; +let fields1: FieldPacket[] | undefined; +let rows2: RowDataPacket[] | undefined; +let fields2: FieldPacket[] | undefined; +let rows3: RowDataPacket[] | undefined; +let fields3: FieldPacket[] | undefined; + +let rows4: RowDataPacket[] | undefined; +let fields4: FieldPacket[] | undefined; +let rows5: RowDataPacket[] | undefined; +let fields5: FieldPacket[] | undefined; + +const query = "SELECT x'010203'"; +const query1 = "SELECT '010203'"; + +connection.query(query, (err, _rows, _fields) => { + if (err) { + throw err; + } + rows = _rows; + fields = _fields; +}); + +connection.query(query, (err, _rows, _fields) => { + if (err) { + throw err; + } + rows5 = _rows; + fields5 = _fields; +}); + +connection.query(query1, (err, _rows, _fields) => { + if (err) { + throw err; + } + rows1 = _rows; + fields1 = _fields; +}); + +connection.execute(query, [], (err, _rows, _fields) => { + if (err) { + throw err; + } + rows2 = _rows; + fields2 = _fields; +}); + +// repeat same query - test cached fields and parser +connection.execute(query, [], (err, _rows, _fields) => { + if (err) { + throw err; + } + rows4 = _rows; + fields4 = _fields; +}); + +connection.execute(query1, [], (err, _rows, _fields) => { + if (err) { + throw err; + } + rows3 = _rows; + fields3 = _fields; + connection.end(); +}); + +process.on('exit', () => { + assert.deepEqual(rows, [{ "x'010203'": Buffer.from([1, 2, 3]) }]); + assert.equal(fields?.[0].name, "x'010203'"); + assert.deepEqual(rows1, [{ '010203': '010203' }]); + assert.equal(fields1?.[0].name, '010203'); + assert.deepEqual(rows2, [{ "x'010203'": Buffer.from([1, 2, 3]) }]); + assert.equal(fields2?.[0].name, "x'010203'"); + assert.deepEqual(rows3, [{ '010203': '010203' }]); + assert.equal(fields3?.[0].name, '010203'); + + assert.deepEqual(rows4, [{ "x'010203'": Buffer.from([1, 2, 3]) }]); + assert.equal(fields4?.[0].name, "x'010203'"); + assert.deepEqual(rows5, [{ "x'010203'": Buffer.from([1, 2, 3]) }]); + assert.equal(fields5?.[0].name, "x'010203'"); +}); diff --git a/test/esm/integration/connection/test-binary-longlong.test.mts b/test/esm/integration/connection/test-binary-longlong.test.mts new file mode 100644 index 0000000000..dced1d7f48 --- /dev/null +++ b/test/esm/integration/connection/test-binary-longlong.test.mts @@ -0,0 +1,125 @@ +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +type LongLongRow = { + id: number; + ls: number | string | null; + lu: number | string | null; +}; + +const conn = createConnection(); + +conn.query( + 'CREATE TEMPORARY TABLE `tmp_longlong` ( ' + + ' `id` int(11) NOT NULL AUTO_INCREMENT, ' + + ' `ls` BIGINT SIGNED, ' + + ' `lu` BIGINT UNSIGNED, ' + + ' PRIMARY KEY (`id`) ' + + ' ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8' +); + +const values = [ + ['10', '10'], + ['-11', '11'], + ['965432100123456789', '1965432100123456789'], + ['-965432100123456789', '2965432100123456789'], + [null, null], +]; + +conn.connect((err) => { + if (err) { + console.error(err); + return; + } + + for (let i = 0; i < values.length; ++i) { + conn.query('INSERT INTO `tmp_longlong` VALUES (?, ?, ?)', [ + i + 1, + values[i][0], + values[i][1], + ]); + } + + const bigNums_bnStringsFalse = [ + { id: 1, ls: 10, lu: 10 }, + { id: 2, ls: -11, lu: 11 }, + { id: 3, ls: 965432100123456800, lu: 1965432100123456800 }, + { id: 4, ls: -965432100123456800, lu: 2965432100123457000 }, + { id: 5, ls: null, lu: null }, + ]; + + const bigNums_bnStringsTrueFalse = [ + { id: 1, ls: 10, lu: 10 }, + { id: 2, ls: -11, lu: 11 }, + { id: 3, ls: '965432100123456789', lu: '1965432100123456789' }, + { id: 4, ls: '-965432100123456789', lu: '2965432100123456789' }, + { id: 5, ls: null, lu: null }, + ]; + + const bigNums_bnStringsTrueTrue = [ + { id: 1, ls: 10, lu: 10 }, + { id: 2, ls: -11, lu: 11 }, + { id: 3, ls: '965432100123456789', lu: '1965432100123456789' }, + { id: 4, ls: '-965432100123456789', lu: '2965432100123456789' }, + { id: 5, ls: null, lu: null }, + ]; + + let completed = 0; + let started = 0; + + function testQuery( + supportBigNumbers: boolean, + bigNumberStrings: boolean, + expectation: LongLongRow[] + ) { + started++; + conn.query( + { + sql: 'SELECT * from tmp_longlong', + // @ts-expect-error: TODO: implement typings + supportBigNumbers: supportBigNumbers, + bigNumberStrings: bigNumberStrings, + }, + (err, rows) => { + assert.ifError(err); + assert.deepEqual(rows, expectation); + completed++; + if (completed === started) { + conn.end(); + } + } + ); + } + + function testExecute( + supportBigNumbers: boolean, + bigNumberStrings: boolean, + expectation: LongLongRow[] + ) { + started++; + conn.execute( + { + sql: 'SELECT * from tmp_longlong', + // @ts-expect-error: TODO: implement typings + supportBigNumbers: supportBigNumbers, + bigNumberStrings: bigNumberStrings, + }, + (err, rows) => { + assert.ifError(err); + assert.deepEqual(rows, expectation); + completed++; + if (completed === started) { + conn.end(); + } + } + ); + } + + testQuery(false, false, bigNums_bnStringsFalse); + testQuery(true, false, bigNums_bnStringsTrueFalse); + testQuery(true, true, bigNums_bnStringsTrueTrue); + + testExecute(false, false, bigNums_bnStringsFalse); + testExecute(true, false, bigNums_bnStringsTrueFalse); + testExecute(true, true, bigNums_bnStringsTrueTrue); +}); diff --git a/test/esm/integration/connection/test-binary-multiple-results.test.mts b/test/esm/integration/connection/test-binary-multiple-results.test.mts new file mode 100644 index 0000000000..3a46ffab47 --- /dev/null +++ b/test/esm/integration/connection/test-binary-multiple-results.test.mts @@ -0,0 +1,223 @@ +// This file was modified by Oracle on June 2, 2021. +// The test has been updated to remove all expectations with regards to the +// "columnLength" metadata field. +// Modifications copyright (c) 2021, Oracle and/or its affiliates. + +import process from 'node:process'; +// @ts-expect-error: no typings available +import assert from 'assert-diff'; +import { createConnection } from '../../common.test.mjs'; + +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test for planetscale'); + process.exit(0); +} + +const mysql = createConnection({ + multipleStatements: true, +}); + +mysql.query('CREATE TEMPORARY TABLE no_rows (test int)'); +mysql.query('CREATE TEMPORARY TABLE some_rows (test int)'); +mysql.query('INSERT INTO some_rows values(0)'); +mysql.query('INSERT INTO some_rows values(42)'); +mysql.query('INSERT INTO some_rows values(314149)'); + +const clone = function (obj: T): T { + return JSON.parse(JSON.stringify(obj)) as T; +}; + +const rs1 = { + affectedRows: 0, + fieldCount: 0, + insertId: 0, + serverStatus: 10, + warningStatus: 0, + info: '', + changedRows: 0, +}; +const rs2 = clone(rs1); +rs2.serverStatus = 2; +const rs3 = clone(rs1); +rs3.serverStatus = 34; + +const select1 = [{ 1: '1' }]; +const select2 = [{ 2: '2' }]; +const fields1 = [ + { + catalog: 'def', + characterSet: 63, + encoding: 'binary', + type: 8, + decimals: 0, + flags: 129, + name: '1', + orgName: '', + orgTable: '', + schema: '', + table: '', + }, +]; +const nr_fields = [ + { + catalog: 'def', + characterSet: 63, + encoding: 'binary', + type: 3, + decimals: 0, + flags: 0, + name: 'test', + orgName: 'test', + orgTable: 'no_rows', + schema: mysql.config.database, + table: 'no_rows', + }, +]; + +const sr_fields = clone(nr_fields); +sr_fields[0].orgTable = 'some_rows'; +sr_fields[0].table = 'some_rows'; +const select3 = [{ test: 0 }, { test: 42 }, { test: 314149 }]; + +const fields2 = clone(fields1); +fields2[0].name = '2'; + +const tests: [string, unknown[]][] = [ + ['select * from some_rows', [[select3, rs3], [sr_fields, undefined], 2]], // select 3 rows + [ + 'SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT; SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS', + [rs2, undefined, 1], + ], + ['set @a = 1', [rs2, undefined, 1]], + ['set @a = 1; set @b = 2', [rs2, undefined, 1]], + [ + 'select 1; select 2', + [[select1, select2, rs2], [fields1, fields2, undefined], 3], + ], + ['set @a = 1; select 1', [[select1, rs2], [fields1, undefined], 2]], + ['select 1; set @a = 1', [[select1, rs2], [fields1, undefined], 2]], + ['select * from no_rows', [[[], rs3], [nr_fields, undefined], 2]], // select 0 rows" + ['set @a = 1; select * from no_rows', [[[], rs3], [nr_fields, undefined], 2]], // insert + select 0 rows + ['select * from no_rows; set @a = 1', [[[], rs3], [nr_fields, undefined], 2]], // select 0 rows + insert + [ + 'set @a = 1; select * from some_rows', + [[select3, rs3], [sr_fields, undefined], 2], + ], // insert + select 3 rows + [ + 'select * from some_rows; set @a = 1', + [[select3, rs3], [sr_fields, undefined], 2], + ], // select 3 rows + insert +]; + +function procedurise(sql: string) { + return [ + 'DROP PROCEDURE IF EXISTS _as_sp_call;', + 'CREATE PROCEDURE _as_sp_call()', + 'BEGIN', + `${sql};`, + 'END', + ].join('\n'); +} + +function do_test(testIndex: number) { + const next = function () { + if (testIndex + 1 < tests.length) { + do_test(testIndex + 1); + } else { + mysql.end(); + } + }; + + const entry = tests[testIndex]; + let sql = entry[0]; + const expectation = entry[1]; + // prepared statements do not support multiple statements itself, we need to wrap quey in a stored procedure + const sp = procedurise(sql); + mysql.query(sp, (err) => { + if (err) { + throw err; + } + + sql = 'CALL _as_sp_call()'; // this call is allowed with prepared statements, and result contain multiple statements + let _numResults = 0; + const textCmd = mysql.query(sql, (err, _rows, _columns) => { + if (err) { + throw err; + } + + const arrOrColumn = function (c: unknown): unknown { + if (Array.isArray(c)) { + return c.map(arrOrColumn); + } + + if (typeof c === 'undefined') { + return void 0; + } + + // @ts-expect-error: internal access + const column = c.inspect() as Record; + // "columnLength" is non-deterministic and the display width for integer + // data types was deprecated on MySQL 8.0.17. + // https://dev.mysql.com/doc/refman/8.0/en/numeric-type-syntax.html + delete column.columnLength; + + return column; + }; + + assert.deepEqual(expectation[0], _rows); + assert.deepEqual(expectation[1], arrOrColumn(_columns)); + + const q = mysql.execute(sql); + let resIndex = 0; + let rowIndex = 0; + let fieldIndex = -1; + + function checkRow(row: { + constructor: { name: string }; + [key: string]: unknown; + }) { + const index = fieldIndex; + const multiRows = _rows as unknown[]; + if (_numResults === 1) { + assert.equal(index, 0); + if (row.constructor.name === 'ResultSetHeader') { + assert.deepEqual(_rows, row); + } else { + assert.deepEqual(multiRows[rowIndex], row); + } + } else { + if (resIndex !== index) { + rowIndex = 0; + resIndex = index; + } + if (row.constructor.name === 'ResultSetHeader') { + assert.deepEqual(multiRows[index], row); + } else { + assert.deepEqual((multiRows[index] as unknown[])[rowIndex], row); + } + } + rowIndex++; + } + + function checkFields(fields: unknown) { + fieldIndex++; + const index = fieldIndex; + if (_numResults === 1) { + assert.equal(index, 0); + assert.deepEqual(arrOrColumn(_columns), arrOrColumn(fields)); + } else { + assert.deepEqual(arrOrColumn(_columns[index]), arrOrColumn(fields)); + } + } + + q.on('result', checkRow); + q.on('fields', checkFields); + q.on('end', next); + }); + + textCmd.on('fields', () => { + _numResults++; + }); + }); +} +do_test(0); diff --git a/test/esm/integration/connection/test-binary-notnull-nulls.test.mts b/test/esm/integration/connection/test-binary-notnull-nulls.test.mts new file mode 100644 index 0000000000..07c87c5577 --- /dev/null +++ b/test/esm/integration/connection/test-binary-notnull-nulls.test.mts @@ -0,0 +1,71 @@ +import type { RowDataPacket } from '../../../../index.js'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const conn = createConnection(); + +// it's possible to receive null values for columns marked with NOT_NULL flag +// see https://github.com/sidorares/node-mysql2/issues/178 for info + +conn.query('set sql_mode=""'); + +conn.query( + 'CREATE TEMPORARY TABLE `tmp_account` ( ' + + ' `id` int(11) NOT NULL AUTO_INCREMENT, ' + + ' `username` varchar(64) NOT NULL, ' + + ' `auth_code` varchar(30) NOT NULL, ' + + ' `access_token` varchar(30) NOT NULL, ' + + ' `refresh_token` tinytext NOT NULL, ' + + ' PRIMARY KEY (`id`) ' + + ' ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8' +); +conn.query("INSERT INTO `tmp_account` VALUES ('1', 'xgredx', '', '', '')"); + +conn.query( + 'CREATE TEMPORARY TABLE `tmp_account_flags` ( ' + + ' `account` int(11) NOT NULL, ' + + ' `flag` tinyint(3) NOT NULL, ' + + ' PRIMARY KEY (`account`,`flag`) ' + + ' ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8' +); + +conn.query("INSERT INTO `tmp_account_flags` VALUES ('1', '100')"); + +conn.query( + 'CREATE TEMPORARY TABLE `tmp_account_session` ( ' + + ' `account` int(11) NOT NULL, ' + + ' `ip` varchar(15) NOT NULL, ' + + ' `session` varchar(114) NOT NULL, ' + + ' `time` int(11) NOT NULL, ' + + ' PRIMARY KEY (`account`,`ip`) ' + + ' ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8' +); + +conn.query( + "INSERT INTO `tmp_account_session` VALUES ('1', '::1', '75efb145482ce22f4544390cad233c749c1b43e4', '1')" +); + +conn.connect((err) => { + if (err) { + console.error(err); + return; + } + + conn.execute( + "SELECT `ac`.`username`, CONCAT('[', GROUP_CONCAT(DISTINCT `acf`.`flag` SEPARATOR ','), ']') flags FROM tmp_account ac LEFT JOIN tmp_account_flags acf ON `acf`.account = `ac`.id LEFT JOIN tmp_account_session acs ON `acs`.account = `ac`.id WHERE `acs`.`session`=?", + ['asid=75efb145482ce22f4544390cad233c749c1b43e4'], + (_err, rows, fields) => { + /* + this assertion is valid for mysql8 < 8.0.17 and not longer valid in 8.0.18 + TODO: investigate why and remove + const flagNotNull = fields[0].flags & FieldFlags.NOT_NULL; + const valueIsNull = rows[0][fields[0].name] === null; + assert(flagNotNull && valueIsNull); + */ + + const valueIsNull = rows[0][fields[0].name] === null; + assert(valueIsNull); + conn.end(); + } + ); +}); diff --git a/test/esm/integration/connection/test-buffer-params.test.mts b/test/esm/integration/connection/test-buffer-params.test.mts new file mode 100644 index 0000000000..41f18d0b17 --- /dev/null +++ b/test/esm/integration/connection/test-buffer-params.test.mts @@ -0,0 +1,43 @@ +import type { QueryError, RowDataPacket } from '../../../../index.js'; +import { Buffer } from 'node:buffer'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +type BufRow = RowDataPacket & { buf: string }; + +const connection = createConnection(); + +let rows: BufRow[] | undefined = undefined; +let rows1: BufRow[] | undefined = undefined; + +const buf = Buffer.from([ + 0x80, 0x90, 1, 2, 3, 4, 5, 6, 7, 8, 9, 100, 100, 255, 255, +]); +connection.execute( + 'SELECT HEX(?) as buf', + [buf], + (err: QueryError | null, _rows) => { + if (err) { + throw err; + } + rows = _rows; + } +); + +connection.query( + 'SELECT HEX(?) as buf', + [buf], + (err: QueryError | null, _rows) => { + if (err) { + throw err; + } + rows1 = _rows; + connection.end(); + } +); + +process.on('exit', () => { + assert.deepEqual(rows, [{ buf: buf.toString('hex').toUpperCase() }]); + assert.deepEqual(rows1, [{ buf: buf.toString('hex').toUpperCase() }]); +}); diff --git a/test/esm/integration/connection/test-change-user-multi-factor.test.mts b/test/esm/integration/connection/test-change-user-multi-factor.test.mts new file mode 100644 index 0000000000..7b8ddd9660 --- /dev/null +++ b/test/esm/integration/connection/test-change-user-multi-factor.test.mts @@ -0,0 +1,161 @@ +// Copyright (c) 2021, Oracle and/or its affiliates. + +import type { Connection } from '../../../../index.js'; +import { Buffer } from 'node:buffer'; +import process from 'node:process'; +import { assert } from 'poku'; +import portfinder from 'portfinder'; +import mysql from '../../../../index.js'; +import Command from '../../../../lib/commands/command.js'; +import Packets from '../../../../lib/packets/index.js'; + +type AuthFactorConfig = { + pluginName: string; + pluginData: Buffer; +}; + +type AuthPluginMetadata = { + connection: Connection; + command: string; +}; + +// The process is not terminated in Deno +if (typeof Deno !== 'undefined') process.exit(0); + +class TestChangeUserMultiFactor extends Command { + args: AuthFactorConfig[]; + authFactor: number; + serverHello: unknown; + + constructor(args: AuthFactorConfig[]) { + super(); + this.args = args; + this.authFactor = 0; + } + + start(_packet: unknown, connection: Connection) { + // @ts-expect-error: TODO: implement typings + const serverHelloPacket = new Packets.Handshake({ + // "required" properties + serverVersion: 'node.js rocks', + // the server should announce support for the + // "MULTI_FACTOR_AUTHENTICATION" capability + capabilityFlags: 0xdfffffff, + }); + this.serverHello = serverHelloPacket; + serverHelloPacket.setScrambleData(() => { + connection.writePacket(serverHelloPacket.toPacket(0)); + }); + return TestChangeUserMultiFactor.prototype.acceptConnection; + } + + acceptConnection(_packet: unknown, connection: Connection) { + connection.writeOk(); + return TestChangeUserMultiFactor.prototype.readChangeUser; + } + + readChangeUser(_packet: unknown, connection: Connection) { + // @ts-expect-error: TODO: implement typings + const asr = new Packets.AuthSwitchRequest(this.args[this.authFactor]); + connection.writePacket(asr.toPacket()); + return TestChangeUserMultiFactor.prototype.sendAuthNextFactor; + } + + sendAuthNextFactor( + _packet: unknown, + connection: Connection + ): (_packet: unknown, connection: Connection) => unknown { + console.log('this.authFactor:', this.authFactor); + // const asr = Packets.AuthSwitchResponse.fromPacket(packet); + // assert.deepStrictEqual(asr.data.toString(), this.args[this.authFactor].pluginName); + if (this.authFactor === 1) { + // send OK_Packet after the 3rd authentication factor + connection.writeOk(); + return TestChangeUserMultiFactor.prototype.dispatchCommands; + } + this.authFactor += 1; + // @ts-expect-error: TODO: implement typings + const anf = new Packets.AuthNextFactor(this.args[this.authFactor]); + // @ts-expect-error: TODO: implement typings + const encoding = connection.serverConfig.encoding; + connection.writePacket(anf.toPacket(encoding)); + return TestChangeUserMultiFactor.prototype.sendAuthNextFactor; + } + + dispatchCommands(_packet: unknown, connection: Connection) { + connection.end(); + return TestChangeUserMultiFactor.prototype.dispatchCommands; + } +} + +const server = mysql.createServer((conn: Connection) => { + // @ts-expect-error: TODO: implement typings + conn.serverConfig = {}; + // @ts-expect-error: TODO: implement typings + conn.serverConfig.encoding = 'cesu8'; + // @ts-expect-error: TODO: implement typings + conn.addCommand( + new TestChangeUserMultiFactor([ + { + // already covered by test-auth-switch + pluginName: 'auth_test_plugin1', + pluginData: Buffer.from('foo'), + }, + { + // 2nd factor auth plugin + pluginName: 'auth_test_plugin2', + pluginData: Buffer.from('bar'), + }, + ]) + ); +}); + +const completed: string[] = []; +const password1 = 'secret1'; +const password2 = 'secret2'; + +portfinder.getPort((_: Error | null, port: number) => { + server.listen(port); + const conn = mysql.createConnection({ + port: port, + authPlugins: { + auth_test_plugin1(options: AuthPluginMetadata) { + return () => { + if (options.connection.config.password !== password1) { + return assert.fail('Incorrect authentication factor password.'); + } + + const pluginName = 'auth_test_plugin1'; + completed.push(pluginName); + + return Buffer.from(pluginName); + }; + }, + auth_test_plugin2(options: AuthPluginMetadata) { + return () => { + if (options.connection.config.password !== password2) { + return assert.fail('Incorrect authentication factor password.'); + } + + const pluginName = 'auth_test_plugin2'; + completed.push(pluginName); + + return Buffer.from(pluginName); + }; + }, + }, + }); + + conn.on('connect', () => { + conn.changeUser({ password1, password2 }, () => { + assert.deepStrictEqual(completed, [ + 'auth_test_plugin1', + 'auth_test_plugin2', + ]); + + conn.end(); + // @ts-expect-error: TODO: implement typings + server.close(); + }); + }); +}); diff --git a/test/esm/integration/connection/test-change-user-plugin-auth.test.mts b/test/esm/integration/connection/test-change-user-plugin-auth.test.mts new file mode 100644 index 0000000000..358a02e938 --- /dev/null +++ b/test/esm/integration/connection/test-change-user-plugin-auth.test.mts @@ -0,0 +1,85 @@ +import type { RowDataPacket } from '../../../../index.js'; +import { Buffer } from 'node:buffer'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test for planetscale'); + process.exit(0); +} + +const connection = createConnection(); +const onlyUsername = function (name: string) { + return name.substring(0, name.indexOf('@')); +}; + +connection.query( + "CREATE USER IF NOT EXISTS 'changeuser1'@'%' IDENTIFIED BY 'changeuser1pass'" +); +connection.query( + "CREATE USER IF NOT EXISTS 'changeuser2'@'%' IDENTIFIED BY 'changeuser2pass'" +); +connection.query("GRANT ALL ON *.* TO 'changeuser1'@'%'"); +connection.query("GRANT ALL ON *.* TO 'changeuser2'@'%'"); +connection.query('FLUSH PRIVILEGES'); + +connection.changeUser( + { + user: 'changeuser1', + password: 'changeuser1pass', + }, + (err) => { + assert.ifError(err); + connection.query('select current_user()', (err, rows) => { + assert.ifError(err); + assert.deepEqual(onlyUsername(rows[0]['current_user()']), 'changeuser1'); + + connection.changeUser( + { + user: 'changeuser2', + password: 'changeuser2pass', + }, + (err) => { + assert.ifError(err); + + connection.query( + 'select current_user()', + (err, rows) => { + assert.ifError(err); + assert.deepEqual( + onlyUsername(rows[0]['current_user()']), + 'changeuser2' + ); + + connection.changeUser( + { + user: 'changeuser1', + password: 'changeuser1pass', + // @ts-expect-error: TODO: implement typings + passwordSha1: Buffer.from( + 'f961d39c82138dcec42b8d0dcb3e40a14fb7e8cd', + 'hex' + ), // sha1(changeuser1pass) + }, + () => { + connection.query( + 'select current_user()', + (err, rows) => { + assert.ifError(err); + assert.deepEqual( + onlyUsername(rows[0]['current_user()']), + 'changeuser1' + ); + connection.end(); + } + ); + } + ); + } + ); + } + ); + }); + } +); diff --git a/test/esm/integration/connection/test-change-user.test.mts b/test/esm/integration/connection/test-change-user.test.mts new file mode 100644 index 0000000000..358a02e938 --- /dev/null +++ b/test/esm/integration/connection/test-change-user.test.mts @@ -0,0 +1,85 @@ +import type { RowDataPacket } from '../../../../index.js'; +import { Buffer } from 'node:buffer'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test for planetscale'); + process.exit(0); +} + +const connection = createConnection(); +const onlyUsername = function (name: string) { + return name.substring(0, name.indexOf('@')); +}; + +connection.query( + "CREATE USER IF NOT EXISTS 'changeuser1'@'%' IDENTIFIED BY 'changeuser1pass'" +); +connection.query( + "CREATE USER IF NOT EXISTS 'changeuser2'@'%' IDENTIFIED BY 'changeuser2pass'" +); +connection.query("GRANT ALL ON *.* TO 'changeuser1'@'%'"); +connection.query("GRANT ALL ON *.* TO 'changeuser2'@'%'"); +connection.query('FLUSH PRIVILEGES'); + +connection.changeUser( + { + user: 'changeuser1', + password: 'changeuser1pass', + }, + (err) => { + assert.ifError(err); + connection.query('select current_user()', (err, rows) => { + assert.ifError(err); + assert.deepEqual(onlyUsername(rows[0]['current_user()']), 'changeuser1'); + + connection.changeUser( + { + user: 'changeuser2', + password: 'changeuser2pass', + }, + (err) => { + assert.ifError(err); + + connection.query( + 'select current_user()', + (err, rows) => { + assert.ifError(err); + assert.deepEqual( + onlyUsername(rows[0]['current_user()']), + 'changeuser2' + ); + + connection.changeUser( + { + user: 'changeuser1', + password: 'changeuser1pass', + // @ts-expect-error: TODO: implement typings + passwordSha1: Buffer.from( + 'f961d39c82138dcec42b8d0dcb3e40a14fb7e8cd', + 'hex' + ), // sha1(changeuser1pass) + }, + () => { + connection.query( + 'select current_user()', + (err, rows) => { + assert.ifError(err); + assert.deepEqual( + onlyUsername(rows[0]['current_user()']), + 'changeuser1' + ); + connection.end(); + } + ); + } + ); + } + ); + } + ); + }); + } +); diff --git a/test/esm/integration/connection/test-charset-encoding.test.mts b/test/esm/integration/connection/test-charset-encoding.test.mts new file mode 100644 index 0000000000..20a9a368ad --- /dev/null +++ b/test/esm/integration/connection/test-charset-encoding.test.mts @@ -0,0 +1,64 @@ +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +type CharsetRow = RowDataPacket & { field: string }; + +const connection = createConnection(); + +// test data stores +const testData = [ + 'ютф восемь', + 'Experimental', + 'परीक्षण', + 'test тест テスト փորձաsրկում পরীক্ষা kiểm tra', + 'ტესტი પરીક્ષણ מבחן פּרובירן اختبار', +]; + +let resultData: CharsetRow[] | null = null; + +// test inserting of non latin data if we are able to parse it + +const testEncoding = function (err: NodeJS.ErrnoException | null) { + assert.ifError(err); + + testData.forEach((data) => { + connection.query( + 'INSERT INTO `test-charset-encoding` (field) values(?)', + [data], + (err2) => { + assert.ifError(err2); + } + ); + }); + + connection.query( + 'SELECT * from `test-charset-encoding`', + (err, results) => { + assert.ifError(err); + resultData = results; + } + ); + connection.end(); +}; + +// init test sequence +(function () { + connection.query('DROP TABLE IF EXISTS `test-charset-encoding`', () => { + connection.query( + 'CREATE TABLE IF NOT EXISTS `test-charset-encoding` ' + + '( `field` VARCHAR(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci)', + (err) => { + assert.ifError(err); + connection.query('DELETE from `test-charset-encoding`', testEncoding); + } + ); + }); +})(); + +process.on('exit', () => { + resultData?.forEach((data, index) => { + assert.equal(data.field, testData[index]); + }); +}); diff --git a/test/esm/integration/connection/test-column-inspect.test.mts b/test/esm/integration/connection/test-column-inspect.test.mts index 308ceba24c..2ab1e1f3b9 100644 --- a/test/esm/integration/connection/test-column-inspect.test.mts +++ b/test/esm/integration/connection/test-column-inspect.test.mts @@ -1,6 +1,6 @@ -import { assert, describe, afterEach, beforeEach, it } from 'poku'; -import util from 'node:util'; import type { Connection as PromiseConnection } from '../../../../promise.js'; +import util from 'node:util'; +import { afterEach, assert, beforeEach, describe, it } from 'poku'; import { config, createConnection, version } from '../../common.test.mjs'; const { database: currentDatabase } = config; diff --git a/test/esm/integration/connection/test-connect-after-connection-error.test.mts b/test/esm/integration/connection/test-connect-after-connection-error.test.mts new file mode 100644 index 0000000000..206640c3f8 --- /dev/null +++ b/test/esm/integration/connection/test-connect-after-connection-error.test.mts @@ -0,0 +1,47 @@ +import type { Connection, QueryError } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import portfinder from 'portfinder'; +import mysql from '../../../../index.js'; + +// The process is not terminated in Deno +if (typeof Deno !== 'undefined') process.exit(0); + +const ERROR_TEXT = 'Connection lost: The server closed the connection.'; + +portfinder.getPort((_err: Error | null, port: number) => { + // @ts-expect-error: TODO: implement typings + const server = mysql.createServer(); + let serverConnection: Connection | undefined; + server.listen(port); + server.on('connection', (conn: Connection) => { + conn.serverHandshake({ + serverVersion: '5.6.10', + capabilityFlags: 2181036031, + }); + serverConnection = conn; + }); + + const clientConnection = mysql.createConnection({ + host: 'localhost', + port: port, + user: 'testuser', + database: 'testdatabase', + password: 'testpassword', + }); + + clientConnection.on('connect', () => { + // @ts-expect-error: TODO: implement typings + serverConnection.close(); + }); + + clientConnection.once('error', () => { + clientConnection.connect((err: QueryError | null) => { + assert.equal(err?.message, ERROR_TEXT); + // @ts-expect-error: TODO: implement typings + clientConnection.close(); + // @ts-expect-error: internal access + server._server.close(); + }); + }); +}); diff --git a/test/esm/integration/connection/test-connect-after-connection.test.mts b/test/esm/integration/connection/test-connect-after-connection.test.mts new file mode 100644 index 0000000000..d04286c242 --- /dev/null +++ b/test/esm/integration/connection/test-connect-after-connection.test.mts @@ -0,0 +1,23 @@ +import type { Connection, QueryError } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +let connection2: Connection | undefined; + +connection.once('connect', () => { + // @ts-expect-error: TODO: implement typings + connection.connect((err: QueryError | null, _connection: Connection) => { + if (err) { + throw err; + } + connection2 = _connection; + connection.end(); + }); +}); + +process.on('exit', () => { + assert.equal(connection, connection2); +}); diff --git a/test/esm/integration/connection/test-connect-connection-closed-error.test.mts b/test/esm/integration/connection/test-connect-connection-closed-error.test.mts new file mode 100644 index 0000000000..48f1ae5595 --- /dev/null +++ b/test/esm/integration/connection/test-connect-connection-closed-error.test.mts @@ -0,0 +1,34 @@ +import type { Connection } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import portfinder from 'portfinder'; +import mysql from '../../../../index.js'; + +// The process is not terminated in Deno +if (typeof Deno !== 'undefined') process.exit(0); + +const ERROR_TEXT = 'Connection lost: The server closed the connection.'; + +portfinder.getPort((_err, port) => { + // @ts-expect-error: TODO: implement typings + const server = mysql.createServer(); + server.listen(port); + server.on('connection', (conn: Connection) => { + // @ts-expect-error: TODO: implement typings + conn.close(); + }); + + const connection = mysql.createConnection({ + host: 'localhost', + port: port, + user: 'testuser', + database: 'testdatabase', + password: 'testpassword', + }); + + connection.query('select 1', (err) => { + assert.equal(err?.message, ERROR_TEXT); + // @ts-expect-error: internal access + server._server.close(); + }); +}); diff --git a/test/esm/integration/connection/test-connect-sha1.test.mts b/test/esm/integration/connection/test-connect-sha1.test.mts new file mode 100644 index 0000000000..e435ca0968 --- /dev/null +++ b/test/esm/integration/connection/test-connect-sha1.test.mts @@ -0,0 +1,93 @@ +import type { Connection, QueryError } from '../../../../index.js'; +import { Buffer } from 'node:buffer'; +import process from 'node:process'; +import { assert } from 'poku'; +import portfinder from 'portfinder'; +import mysql from '../../../../index.js'; +import auth from '../../../../lib/auth_41.js'; + +type AuthParams = { + authPluginData1: Buffer; + authPluginData2: Buffer; + authToken: Buffer; +}; + +// The process is not terminated in Deno +if (typeof Deno !== 'undefined') process.exit(0); + +function authenticate(params: AuthParams, cb: (err: Error | null) => void) { + const doubleSha = auth.doubleSha1('testpassword'); + const isValid = auth.verifyToken( + params.authPluginData1, + params.authPluginData2, + params.authToken, + doubleSha + ); + assert(isValid); + cb(null); +} + +let _1_2 = false; +let _1_3 = false; + +let queryCalls = 0; + +portfinder.getPort((_err, port) => { + // @ts-expect-error: TODO: implement typings + const server = mysql.createServer(); + server.listen(port); + server.on('connection', (conn: Connection) => { + conn.serverHandshake({ + protocolVersion: 10, + serverVersion: 'node.js rocks', + connectionId: 1234, + statusFlags: 2, + characterSet: 8, + capabilityFlags: 0xffffff, + authCallback: authenticate, + }); + conn.on('query', (sql: string) => { + assert.equal(sql, 'select 1+1'); + queryCalls++; + // @ts-expect-error: TODO: implement typings + conn.close(); + }); + }); + + // @ts-expect-error: TODO: implement typings + const connection = mysql.createConnection({ + port: port, + user: 'testuser', + database: 'testdatabase', + passwordSha1: Buffer.from( + '8bb6118f8fd6935ad0876a3be34a717d32708ffd', + 'hex' + ), + }); + + connection.on('error', (err: QueryError) => { + assert.equal(err.code, 'PROTOCOL_CONNECTION_LOST'); + }); + + connection.query('select 1+1', (err: QueryError | null) => { + assert.equal(err?.code, 'PROTOCOL_CONNECTION_LOST'); + // @ts-expect-error: internal access + server._server.close(); + }); + + connection.query('select 1+2', (err: QueryError | null) => { + assert.equal(err?.code, 'PROTOCOL_CONNECTION_LOST'); + _1_2 = true; + }); + + connection.query('select 1+3', (err: QueryError | null) => { + assert.equal(err?.code, 'PROTOCOL_CONNECTION_LOST'); + _1_3 = true; + }); +}); + +process.on('exit', () => { + assert.equal(queryCalls, 1); + assert.equal(_1_2, true); + assert.equal(_1_3, true); +}); diff --git a/test/esm/integration/connection/test-connect-time-error.test.mts b/test/esm/integration/connection/test-connect-time-error.test.mts new file mode 100644 index 0000000000..d33554329a --- /dev/null +++ b/test/esm/integration/connection/test-connect-time-error.test.mts @@ -0,0 +1,41 @@ +import type { Connection } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import portfinder from 'portfinder'; +import mysql from '../../../../index.js'; + +// The process is not terminated in Deno +if (typeof Deno !== 'undefined') process.exit(0); + +const ERROR_TEXT = 'test error'; + +portfinder.getPort((_err, port) => { + // @ts-expect-error: TODO: implement typings + const server = mysql.createServer(); + server.listen(port); + server.on('connection', (conn: Connection) => { + conn.writeError(new Error(ERROR_TEXT)); + // @ts-expect-error: TODO: implement typings + conn.close(); + }); + + const connection = mysql.createConnection({ + host: 'localhost', + port: port, + user: 'testuser', + database: 'testdatabase', + password: 'testpassword', + }); + + connection.query('select 1+1', (err) => { + assert.equal(err?.message, ERROR_TEXT); + }); + + connection.query('select 1+2', (err) => { + assert.equal(err?.message, ERROR_TEXT); + // @ts-expect-error: TODO: implement typings + connection.close(); + // @ts-expect-error: internal access + server._server.close(); + }); +}); diff --git a/test/esm/integration/connection/test-connect-with-uri.test.mts b/test/esm/integration/connection/test-connect-with-uri.test.mts new file mode 100644 index 0000000000..badcbad339 --- /dev/null +++ b/test/esm/integration/connection/test-connect-with-uri.test.mts @@ -0,0 +1,30 @@ +import type { FieldPacket, RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnectionWithURI } from '../../common.test.mjs'; + +if (process.env.MYSQL_CONNECTION_URL) { + console.log( + 'skipping test when mysql server is configured using MYSQL_CONNECTION_URL' + ); + process.exit(0); +} + +const connection = createConnectionWithURI(); + +let rows: RowDataPacket[] | undefined = undefined; +let fields: FieldPacket[] | undefined = undefined; +connection.query('SELECT 1', (err, _rows, _fields) => { + if (err) { + throw err; + } + + rows = _rows; + fields = _fields; + connection.end(); +}); + +process.on('exit', () => { + assert.deepEqual(rows, [{ 1: 1 }]); + assert.equal(fields?.[0].name, '1'); +}); diff --git a/test/esm/integration/connection/test-connection-reset-while-closing.test.mts b/test/esm/integration/connection/test-connection-reset-while-closing.test.mts new file mode 100644 index 0000000000..8cc090e3ef --- /dev/null +++ b/test/esm/integration/connection/test-connection-reset-while-closing.test.mts @@ -0,0 +1,29 @@ +import type { RowDataPacket } from '../../../../index.js'; +import assert from 'node:assert'; +import process from 'node:process'; +import { createConnection } from '../../common.test.mjs'; + +const error = new Error('read ECONNRESET') as Error & { + code?: string; + errno?: number; + syscall?: string; +}; +error.code = 'ECONNRESET'; +error.errno = -54; +error.syscall = 'read'; + +const connection = createConnection(); + +// Test that we ignore a ECONNRESET error if the connection +// is already closing, we close and then emit the error +connection.query(`select 1 as "1"`, (_err, rows) => { + assert.equal(rows[0]['1'], 1); + // @ts-expect-error: TODO: implement typings + connection.close(); + // @ts-expect-error: TODO: implement typings + connection.stream.emit('error', error); +}); + +process.on('uncaughtException', (err: Error & { code?: string }) => { + assert.notEqual(err.code, 'ECONNRESET'); +}); diff --git a/test/esm/integration/connection/test-custom-date-parameter.test.mts b/test/esm/integration/connection/test-custom-date-parameter.test.mts new file mode 100644 index 0000000000..639f43d3b2 --- /dev/null +++ b/test/esm/integration/connection/test-custom-date-parameter.test.mts @@ -0,0 +1,36 @@ +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection({ timezone: 'Z' }); + +let rows: RowDataPacket[] | undefined = undefined; + +// @ts-expect-error: intentionally replacing global Date for testing +// eslint-disable-next-line no-global-assign +Date = (function () { + const NativeDate = Date; + function CustomDate(str: string) { + return new NativeDate(str); + } + CustomDate.now = Date.now; + return CustomDate; +})(); + +connection.query("set time_zone = '+00:00'"); +connection.execute( + 'SELECT UNIX_TIMESTAMP(?) t', + [new Date('1990-08-08 UTC')], + (err, _rows) => { + if (err) { + throw err; + } + rows = _rows; + connection.end(); + } +); + +process.on('exit', () => { + assert.equal(rows?.[0].t, 650073600); +}); diff --git a/test/esm/integration/connection/test-date-parameter.test.mts b/test/esm/integration/connection/test-date-parameter.test.mts new file mode 100644 index 0000000000..635873aae9 --- /dev/null +++ b/test/esm/integration/connection/test-date-parameter.test.mts @@ -0,0 +1,25 @@ +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection({ timezone: 'Z' }); + +let rows: RowDataPacket[] | undefined = undefined; + +connection.query("set time_zone = '+00:00'"); +connection.execute( + 'SELECT UNIX_TIMESTAMP(?) t', + [new Date('1990-01-01 UTC')], + (err, _rows) => { + if (err) { + throw err; + } + rows = _rows; + connection.end(); + } +); + +process.on('exit', () => { + assert.deepEqual(rows, [{ t: 631152000 }]); +}); diff --git a/test/esm/integration/connection/test-datetime.test.mts b/test/esm/integration/connection/test-datetime.test.mts new file mode 100644 index 0000000000..443d30fa3d --- /dev/null +++ b/test/esm/integration/connection/test-datetime.test.mts @@ -0,0 +1,337 @@ +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); +const connection1 = createConnection({ dateStrings: true }); +const connection2 = createConnection({ dateStrings: ['DATE'] }); +const connectionZ = createConnection({ timezone: 'Z' }); +const connection0930 = createConnection({ timezone: '+09:30' }); + +let rows: RowDataPacket[], + rowsZ: RowDataPacket[], + rows0930: RowDataPacket[], + rows1: RowDataPacket[], + rows1Z: RowDataPacket[], + rows10930: RowDataPacket[], + rows2: RowDataPacket[], + rows3: RowDataPacket[], + rows4: RowDataPacket[], + rows5: RowDataPacket[], + rows6: RowDataPacket[], + rows7: RowDataPacket[], + rows8: RowDataPacket[]; + +const date = new Date('1990-01-01 08:15:11 UTC'); +const datetime = new Date('2010-12-10 14:12:09.019473'); + +const date1 = new Date('2000-03-03 08:15:11 UTC'); +const date2 = '2010-12-10 14:12:09.019473'; +const date3 = null; +const date4 = '2010-12-10 14:12:09.123456'; +const date5 = '2010-12-10 14:12:09.019'; +const date6 = '2024-11-10 00:00:00'; + +function adjustTZ(d: Date, offset?: number) { + if (offset === undefined) { + offset = d.getTimezoneOffset(); + } + return new Date(d.getTime() - offset * 60000); +} + +function toMidnight(d: Date, offset?: number) { + const t = d.getTime(); + if (offset === undefined) { + offset = d.getTimezoneOffset(); + } + return new Date(t - (t % (24 * 60 * 60 * 1000)) + offset * 60000); +} + +function formatUTCDate(d: Date) { + return d.toISOString().substring(0, 10); +} + +function formatUTCDateTime(d: Date, precision?: number) { + const raw = d.toISOString().replace('T', ' '); + if (precision === undefined) { + precision = 0; + } + return precision <= 3 + ? raw.substring(0, 19 + (precision && 1) + precision) + : raw.substring(0, 23) + '0'.repeat(precision - 3); +} + +connection.query( + 'CREATE TEMPORARY TABLE t (d1 DATE, d2 DATETIME(3), d3 DATETIME(6))' +); +connection.query('INSERT INTO t set d1=?, d2=?, d3=?', [ + date, + datetime, + datetime, +]); + +connection1.query( + 'CREATE TEMPORARY TABLE t (d1 DATE, d2 TIMESTAMP, d3 DATETIME, d4 DATETIME, d5 DATETIME(6), d6 DATETIME(3), d7 DATETIME)' +); +connection1.query( + 'INSERT INTO t set d1=?, d2=?, d3=?, d4=?, d5=?, d6=?, d7=?', + [date, date1, date2, date3, date4, date5, date6] +); + +connection2.query( + 'CREATE TEMPORARY TABLE t (d1 DATE, d2 TIMESTAMP, d3 DATETIME, d4 DATETIME, d5 DATETIME(6), d6 DATETIME(3), d7 DATETIME)' +); +connection2.query( + 'INSERT INTO t set d1=?, d2=?, d3=?, d4=?, d5=?, d6=?, d7=?', + [date, date1, date2, date3, date4, date5, date6] +); + +connectionZ.query( + 'CREATE TEMPORARY TABLE t (d1 DATE, d2 DATETIME(3), d3 DATETIME(6))' +); +connectionZ.query("set time_zone = '+00:00'"); +connectionZ.query('INSERT INTO t set d1=?, d2=?, d3=?', [ + date, + datetime, + datetime, +]); + +connection0930.query( + 'CREATE TEMPORARY TABLE t (d1 DATE, d2 DATETIME(3), d3 DATETIME(6))' +); +connection0930.query("set time_zone = '+09:30'"); +connection0930.query('INSERT INTO t set d1=?, d2=?, d3=?', [ + date, + datetime, + datetime, +]); + +const dateAsStringExpected = [ + { + d1: formatUTCDate(adjustTZ(date)), + d2: formatUTCDateTime(adjustTZ(date1)), + d3: date2.substring(0, 19), + d4: date3, + d5: date4, + d6: date5, + d7: date6, + }, +]; + +connection.execute( + 'select from_unixtime(?) t', + [(+date).valueOf() / 1000], + (err, _rows) => { + if (err) { + throw err; + } + rows = _rows; + } +); + +connectionZ.execute( + 'select from_unixtime(?) t', + [(+date).valueOf() / 1000], + (err, _rows) => { + if (err) { + throw err; + } + rowsZ = _rows; + } +); + +connection0930.execute( + 'select from_unixtime(?) t', + [(+date).valueOf() / 1000], + (err, _rows) => { + if (err) { + throw err; + } + rows0930 = _rows; + } +); + +connection.query( + 'select from_unixtime(631152000) t', + (err, _rows) => { + if (err) { + throw err; + } + rows1 = _rows; + } +); + +connectionZ.query( + 'select from_unixtime(631152000) t', + (err, _rows) => { + if (err) { + throw err; + } + rows1Z = _rows; + } +); + +connection0930.query( + 'select from_unixtime(631152000) t', + (err, _rows) => { + if (err) { + throw err; + } + rows10930 = _rows; + } +); + +connection.query( + 'select *, cast(d1 as char) as d4, cast(d2 as char) as d5, cast(d3 as char) as d6 from t', + (err, _rows) => { + if (err) { + throw err; + } + rows2 = _rows; + connection.end(); + } +); + +connectionZ.execute( + 'select *, cast(d1 as char) as d4, cast(d2 as char) as d5, cast(d3 as char) as d6 from t', + (err, _rows) => { + if (err) { + throw err; + } + rows3 = _rows; + connectionZ.end(); + } +); + +connection1.query('select * from t', (err, _rows) => { + if (err) { + throw err; + } + rows4 = _rows; +}); + +connection1.execute('select * from t', (err, _rows) => { + if (err) { + throw err; + } + rows5 = _rows; +}); + +connection1.execute( + 'select * from t where d6 = ?', + [new Date(date5)], + (err, _rows) => { + if (err) { + throw err; + } + rows6 = _rows; + connection1.end(); + } +); + +connection2.execute('select * from t', (err, _rows) => { + if (err) { + throw err; + } + rows8 = _rows; + connection2.end(); +}); + +connection0930.execute( + 'select *, cast(d1 as char) as d4, cast(d2 as char) as d5, cast(d3 as char) as d6 from t', + (err, _rows) => { + if (err) { + throw err; + } + rows7 = _rows; + connection0930.end(); + } +); + +process.on('exit', () => { + const connBadTz = createConnection({ timezone: 'utc' }); + assert.equal(connBadTz.config.timezone, 'Z'); + connBadTz.end(); + + // local TZ + assert.equal(rows[0].t.constructor, Date); + assert.equal(rows[0].t.getDate(), date.getDate()); + assert.equal(rows[0].t.getHours(), date.getHours()); + assert.equal(rows[0].t.getMinutes(), date.getMinutes()); + assert.equal(rows[0].t.getSeconds(), date.getSeconds()); + + // UTC + assert.equal(rowsZ[0].t.constructor, Date); + assert.equal(rowsZ[0].t.getDate(), date.getDate()); + assert.equal(rowsZ[0].t.getHours(), date.getHours()); + assert.equal(rowsZ[0].t.getMinutes(), date.getMinutes()); + assert.equal(rowsZ[0].t.getSeconds(), date.getSeconds()); + + // +09:30 + assert.equal(rows0930[0].t.constructor, Date); + assert.equal(rows0930[0].t.getDate(), date.getDate()); + assert.equal(rows0930[0].t.getHours(), date.getHours()); + assert.equal(rows0930[0].t.getMinutes(), date.getMinutes()); + assert.equal(rows0930[0].t.getSeconds(), date.getSeconds()); + + // local TZ + assert.equal(rows1[0].t.constructor, Date); + assert.equal( + rows1[0].t.getTime(), + new Date('Mon Jan 01 1990 00:00:00 UTC').getTime() + ); + + // UTC + assert.equal(rows1Z[0].t.constructor, Date); + assert.equal( + rows1Z[0].t.getTime(), + new Date('Mon Jan 01 1990 00:00:00 UTC').getTime() + ); + + // +09:30 + assert.equal(rows10930[0].t.constructor, Date); + assert.equal( + rows10930[0].t.getTime(), + new Date('Mon Jan 01 1990 00:00:00 UTC').getTime() + ); + + // local TZ + assert.equal(rows2[0].d1.getTime(), toMidnight(date).getTime()); + assert.equal(rows2[0].d2.getTime(), datetime.getTime()); + assert.equal(rows2[0].d3.getTime(), datetime.getTime()); + assert.equal(rows2[0].d4, formatUTCDate(adjustTZ(date))); + assert.equal(rows2[0].d5, formatUTCDateTime(adjustTZ(datetime), 3)); + assert.equal(rows2[0].d6, formatUTCDateTime(adjustTZ(datetime), 6)); + + // UTC + assert.equal(rows3[0].d1.getTime(), toMidnight(date, 0).getTime()); + assert.equal(rows3[0].d2.getTime(), datetime.getTime()); + assert.equal(rows3[0].d3.getTime(), datetime.getTime()); + assert.equal(rows3[0].d4, formatUTCDate(date)); + assert.equal(rows3[0].d5, formatUTCDateTime(datetime, 3)); + assert.equal(rows3[0].d6, formatUTCDateTime(datetime, 6)); + + // dateStrings + assert.deepEqual(rows4, dateAsStringExpected); + assert.deepEqual(rows5, dateAsStringExpected); + assert.equal(rows6.length, 1); + + // dateStrings as array + assert.equal(rows8[0].d1, '1990-01-01'); + assert.equal(rows8[0].d1.constructor, String); + assert.equal(rows8[0].d2.constructor, Date); + assert.equal(rows8[0].d3.constructor, Date); + assert.equal(rows8[0].d4, null); + assert.equal(rows8[0].d5.constructor, Date); + assert.equal(rows8[0].d6.constructor, Date); + + // +09:30 + const tzOffset = -570; + assert.equal(rows7[0].d1.getTime(), toMidnight(date, tzOffset).getTime()); + assert.equal(rows7[0].d2.getTime(), datetime.getTime()); + assert.equal(rows7[0].d3.getTime(), datetime.getTime()); + assert.equal(rows7[0].d4, formatUTCDate(adjustTZ(date, tzOffset))); + assert.equal(rows7[0].d5, formatUTCDateTime(adjustTZ(datetime, tzOffset), 3)); + assert.equal(rows7[0].d6, formatUTCDateTime(adjustTZ(datetime, tzOffset), 6)); +}); diff --git a/test/esm/integration/connection/test-decimals-as-numbers.test.mts b/test/esm/integration/connection/test-decimals-as-numbers.test.mts new file mode 100644 index 0000000000..9d273a1ef4 --- /dev/null +++ b/test/esm/integration/connection/test-decimals-as-numbers.test.mts @@ -0,0 +1,38 @@ +import type { RowDataPacket } from '../../../../index.js'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection1 = createConnection({ + decimalNumbers: false, +}); +const connection2 = createConnection({ + decimalNumbers: true, +}); + +const largeDecimal = 900719.547409; +const largeDecimalExpected = '900719.547409000000000000000000000000'; +const largeMoneyValue = 900719925474.99; + +connection1.query('CREATE TEMPORARY TABLE t1 (d1 DECIMAL(65, 30))'); +connection1.query('INSERT INTO t1 set d1=?', [largeDecimal]); + +connection2.query('CREATE TEMPORARY TABLE t2 (d1 DECIMAL(14, 2))'); +connection2.query('INSERT INTO t2 set d1=?', [largeMoneyValue]); + +connection1.execute('select d1 from t1', (err, _rows) => { + if (err) { + throw err; + } + assert.equal(_rows[0].d1.constructor, String); + assert.equal(_rows[0].d1, largeDecimalExpected); + connection1.end(); +}); + +connection2.query('select d1 from t2', (err, _rows) => { + if (err) { + throw err; + } + assert.equal(_rows[0].d1.constructor, Number); + assert.equal(_rows[0].d1, largeMoneyValue); + connection2.end(); +}); diff --git a/test/esm/integration/connection/test-disconnects.test.mts b/test/esm/integration/connection/test-disconnects.test.mts new file mode 100644 index 0000000000..8d1739b1eb --- /dev/null +++ b/test/esm/integration/connection/test-disconnects.test.mts @@ -0,0 +1,89 @@ +// This file was modified by Oracle on January 21, 2021. +// The connection with the mock server needs to happen in the same host where +// the tests are running in order to avoid connecting a potential MySQL server +// instance running in the host identified by the MYSQL_HOST environment +// variable. +// Modifications copyright (c) 2021, Oracle and/or its affiliates. + +import type { + Connection, + FieldPacket, + RowDataPacket, +} from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection, createServer } from '../../common.test.mjs'; + +// The process is not terminated in Deno +if (typeof Deno !== 'undefined') process.exit(0); + +let rows: RowDataPacket[] | undefined; +let fields: FieldPacket[] | undefined; + +const connections: Connection[] = []; + +const server = createServer( + () => { + const connection = createConnection({ + // The mock server is running on the same host machine. + // We need to explicitly define the host to avoid connecting to a potential + // different host provided via MYSQL_HOST that identifies a real MySQL + // server instance. + host: 'localhost', + // @ts-expect-error: internal access + port: server._port, + // @ts-expect-error: TODO: implement typings + ssl: false, + }); + connection.query('SELECT 123', (err, _rows, _fields) => { + if (err) { + throw err; + } + + rows = _rows; + fields = _fields; + connection.on('error', (_err) => { + err = _err; + }); + + connections.forEach((conn) => { + // @ts-expect-error: TODO: implement typings + conn.stream.end(); + }); + // @ts-expect-error: internal access + server._server.close(() => { + assert.equal(err?.code, 'PROTOCOL_CONNECTION_LOST'); + }); + }); + // TODO: test connection.end() etc where we expect disconnect to happen + }, + (conn) => { + connections.push(conn); + conn.on('query', () => { + conn.writeTextResult( + [{ 1: '1' }], + [ + { + catalog: 'def', + schema: '', + table: '', + orgTable: '', + name: '1', + orgName: '', + characterSet: 63, + columnLength: 1, + columnType: 8, + type: 8, + flags: 129, + decimals: 0, + }, + ] + ); + }); + } +); + +process.on('exit', () => { + assert.deepEqual(rows, [{ 1: 1 }]); + assert.equal(fields?.[0].name, '1'); +}); diff --git a/test/esm/integration/connection/test-error-events.test.mts b/test/esm/integration/connection/test-error-events.test.mts new file mode 100644 index 0000000000..02eb27fbc4 --- /dev/null +++ b/test/esm/integration/connection/test-error-events.test.mts @@ -0,0 +1,34 @@ +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +let callCount = 0; +let exceptionCount = 0; + +process.on('uncaughtException', (err) => { + assert.ifError(err); + exceptionCount++; +}); + +const connection1 = createConnection({ + password: 'lol', +}); + +// error will NOT bubble up to process level if `on` is used +connection1.on('error', () => { + callCount++; +}); + +const connection2 = createConnection({ + password: 'lol', +}); + +// error will bubble up to process level if `once` is used +connection2.once('error', () => { + callCount++; +}); + +process.on('exit', () => { + assert.equal(callCount, 2); + assert.equal(exceptionCount, 0); +}); diff --git a/test/esm/integration/connection/test-errors.test.mts b/test/esm/integration/connection/test-errors.test.mts new file mode 100644 index 0000000000..c78f910f98 --- /dev/null +++ b/test/esm/integration/connection/test-errors.test.mts @@ -0,0 +1,61 @@ +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +// different error codes for PS, disabling for now +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test for planetscale'); + process.exit(0); +} + +const connection = createConnection(); + +let onExecuteResultError: boolean | undefined = undefined; +let onQueryResultError: boolean | undefined = undefined; +let onExecuteErrorEvent: boolean | undefined = undefined; +let onQueryErrorEvent: boolean | undefined = undefined; +let onExecuteErrorEvent1: boolean | undefined = undefined; +let onQueryErrorEvent1: boolean | undefined = undefined; + +connection + .execute('error in execute', [], (err) => { + assert.equal(err?.errno, 1064); + assert.equal(err?.code, 'ER_PARSE_ERROR'); + // @ts-expect-error: TODO: implement typings + assert.equal(err?.sql, 'error in execute'); + if (err) { + onExecuteResultError = true; + } + }) + .on('error', () => { + onExecuteErrorEvent = true; + }); +connection + .query('error in query', [], (err) => { + assert.equal(err?.errno, 1064); + assert.equal(err?.code, 'ER_PARSE_ERROR'); + // @ts-expect-error: TODO: implement typings + assert.equal(err?.sql, 'error in query'); + if (err) { + onQueryResultError = true; + } + }) + .on('error', () => { + onQueryErrorEvent = true; + }); +connection.execute('error in execute 1', []).on('error', () => { + onExecuteErrorEvent1 = true; +}); +connection.query('error in query 1').on('error', () => { + onQueryErrorEvent1 = true; + connection.end(); +}); + +process.on('exit', () => { + assert.equal(onExecuteResultError, true); + assert.equal(onQueryResultError, true); + assert.equal(onExecuteErrorEvent, undefined); + assert.equal(onQueryErrorEvent, undefined); + assert.equal(onExecuteErrorEvent1, true); + assert.equal(onQueryErrorEvent1, true); +}); diff --git a/test/esm/integration/connection/test-execute-1.test.mts b/test/esm/integration/connection/test-execute-1.test.mts index 813b8f4fed..149d6d9a79 100644 --- a/test/esm/integration/connection/test-execute-1.test.mts +++ b/test/esm/integration/connection/test-execute-1.test.mts @@ -1,5 +1,5 @@ import type { RowDataPacket } from '../../../../promise.js'; -import { it, assert, describe } from 'poku'; +import { assert, describe, it } from 'poku'; import { createConnection } from '../../common.test.mjs'; await describe(async () => { diff --git a/test/esm/integration/connection/test-execute-and-unprepare.test.mts b/test/esm/integration/connection/test-execute-and-unprepare.test.mts new file mode 100644 index 0000000000..d6826090ab --- /dev/null +++ b/test/esm/integration/connection/test-execute-and-unprepare.test.mts @@ -0,0 +1,25 @@ +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +const max = 500; +function exec(i: number) { + const query = `select 1+${i}`; + connection.execute(query, (err) => { + connection.unprepare(query); + if (err) { + throw err; + } + if (i > max) { + connection.end(); + } else { + exec(i + 1); + } + }); +} +connection.query('SET GLOBAL max_prepared_stmt_count=10', (err) => { + if (err) { + throw err; + } + exec(1); +}); diff --git a/test/esm/integration/connection/test-execute-bind-boolean.test.mts b/test/esm/integration/connection/test-execute-bind-boolean.test.mts new file mode 100644 index 0000000000..359c99dece --- /dev/null +++ b/test/esm/integration/connection/test-execute-bind-boolean.test.mts @@ -0,0 +1,23 @@ +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +let rows: RowDataPacket[]; +connection.execute( + 'SELECT ? AS trueValue, ? AS falseValue', + [true, false], + (err, _rows) => { + if (err) { + throw err; + } + rows = _rows; + connection.end(); + } +); + +process.on('exit', () => { + assert.deepEqual(rows, [{ trueValue: 1, falseValue: 0 }]); +}); diff --git a/test/esm/integration/connection/test-execute-bind-date.test.mts b/test/esm/integration/connection/test-execute-bind-date.test.mts new file mode 100644 index 0000000000..3ea31461fe --- /dev/null +++ b/test/esm/integration/connection/test-execute-bind-date.test.mts @@ -0,0 +1,24 @@ +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); +const date = new Date(2018, 2, 10, 15, 12, 34, 1234); + +let rows: RowDataPacket[]; +connection.execute( + 'SELECT CAST(? AS DATETIME(6)) AS result', + [date], + (err, _rows) => { + if (err) { + throw err; + } + rows = _rows; + connection.end(); + } +); + +process.on('exit', () => { + assert.deepEqual(rows, [{ result: date }]); +}); diff --git a/test/esm/integration/connection/test-execute-bind-function.test.mts b/test/esm/integration/connection/test-execute-bind-function.test.mts new file mode 100644 index 0000000000..fba81c3111 --- /dev/null +++ b/test/esm/integration/connection/test-execute-bind-function.test.mts @@ -0,0 +1,29 @@ +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +let error: Error | null = null; + +try { + connection.execute('SELECT ? AS result', [function () {}], () => {}); +} catch (err) { + if (err instanceof Error) { + error = err; + } else { + throw err; + } + connection.end(); +} + +process.on('exit', () => { + assert(error instanceof Error, 'Expected TypeError to be thrown'); + if (!error) { + return; + } + assert.equal(error.name, 'TypeError'); + if (!error.message.match(/function/)) { + assert.fail("Expected error.message to contain 'function'"); + } +}); diff --git a/test/esm/integration/connection/test-execute-bind-json.test.mts b/test/esm/integration/connection/test-execute-bind-json.test.mts new file mode 100644 index 0000000000..ea4200bff6 --- /dev/null +++ b/test/esm/integration/connection/test-execute-bind-json.test.mts @@ -0,0 +1,25 @@ +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); +const table = 'jsontable'; +const testJson = [{ a: 1, b: true, c: ['foo'] }]; + +let rows: RowDataPacket[]; +connection.query(`CREATE TEMPORARY TABLE ${table} (data JSON)`); +connection.query( + `INSERT INTO ${table} (data) VALUES ('${JSON.stringify(testJson)}')` +); +connection.execute(`SELECT * from ${table}`, (err, _rows) => { + if (err) { + throw err; + } + rows = _rows; + connection.end(); +}); + +process.on('exit', () => { + assert.deepEqual(rows, [{ data: testJson }]); +}); diff --git a/test/esm/integration/connection/test-execute-bind-null.test.mts b/test/esm/integration/connection/test-execute-bind-null.test.mts new file mode 100644 index 0000000000..252c256511 --- /dev/null +++ b/test/esm/integration/connection/test-execute-bind-null.test.mts @@ -0,0 +1,25 @@ +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +let rows: RowDataPacket[]; +connection.execute( + 'SELECT ? AS firstValue, ? AS nullValue, ? AS lastValue', + ['foo', null, 'bar'], + (err, _rows) => { + if (err) { + throw err; + } + rows = _rows; + connection.end(); + } +); + +process.on('exit', () => { + assert.deepEqual(rows, [ + { firstValue: 'foo', nullValue: null, lastValue: 'bar' }, + ]); +}); diff --git a/test/esm/integration/connection/test-execute-bind-number.test.mts b/test/esm/integration/connection/test-execute-bind-number.test.mts new file mode 100644 index 0000000000..5de3888686 --- /dev/null +++ b/test/esm/integration/connection/test-execute-bind-number.test.mts @@ -0,0 +1,30 @@ +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +let rows: RowDataPacket[]; +connection.execute( + 'SELECT ? AS zeroValue, ? AS positiveValue, ? AS negativeValue, ? AS decimalValue', + [0, 123, -123, 1.25], + (err, _rows) => { + if (err) { + throw err; + } + rows = _rows; + connection.end(); + } +); + +process.on('exit', () => { + assert.deepEqual(rows, [ + { + zeroValue: 0, + positiveValue: 123, + negativeValue: -123, + decimalValue: 1.25, + }, + ]); +}); diff --git a/test/esm/integration/connection/test-execute-bind-undefined.test.mts b/test/esm/integration/connection/test-execute-bind-undefined.test.mts new file mode 100644 index 0000000000..6b8381f7f3 --- /dev/null +++ b/test/esm/integration/connection/test-execute-bind-undefined.test.mts @@ -0,0 +1,29 @@ +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +let error: Error | null = null; + +try { + connection.execute('SELECT ? AS result', [undefined], () => {}); +} catch (err) { + if (err instanceof Error) { + error = err; + } else { + throw err; + } + connection.end(); +} + +process.on('exit', () => { + assert(error instanceof Error, 'Expected TypeError to be thrown'); + if (!error) { + return; + } + assert.equal(error.name, 'TypeError'); + if (!error.message.match(/undefined/)) { + assert.fail("Expected error.message to contain 'undefined'"); + } +}); diff --git a/test/esm/integration/connection/test-execute-cached.test.mts b/test/esm/integration/connection/test-execute-cached.test.mts new file mode 100644 index 0000000000..d4630c1680 --- /dev/null +++ b/test/esm/integration/connection/test-execute-cached.test.mts @@ -0,0 +1,47 @@ +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +type TestRow = RowDataPacket & { test: number }; + +const connection = createConnection(); + +let rows: TestRow[] | undefined = undefined; +let rows1: TestRow[] | undefined = undefined; +let rows2: TestRow[] | undefined = undefined; + +const q = 'select 1 + ? as test'; +const key = `undefined/undefined/undefined${q}`; + +connection.execute(q, [123], (err, _rows) => { + if (err) { + throw err; + } + rows = _rows; + connection.execute(q, [124], (err, _rows) => { + if (err) { + throw err; + } + rows1 = _rows; + connection.execute(q, [125], (err, _rows) => { + if (err) { + throw err; + } + rows2 = _rows; + // @ts-expect-error: internal access + assert(connection._statements.size === 1); + // @ts-expect-error: internal access + assert(connection._statements.get(key).query === q); + // @ts-expect-error: internal access + assert(connection._statements.get(key).parameters.length === 1); + connection.end(); + }); + }); +}); + +process.on('exit', () => { + assert.deepEqual(rows, [{ test: 124 }]); + assert.deepEqual(rows1, [{ test: 125 }]); + assert.deepEqual(rows2, [{ test: 126 }]); +}); diff --git a/test/esm/integration/connection/test-execute-newdecimal.test.mts b/test/esm/integration/connection/test-execute-newdecimal.test.mts new file mode 100644 index 0000000000..76a538469d --- /dev/null +++ b/test/esm/integration/connection/test-execute-newdecimal.test.mts @@ -0,0 +1,27 @@ +import type { FieldPacket, RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +connection.query('CREATE TEMPORARY TABLE t (f DECIMAL(19,4))'); +connection.query('INSERT INTO t VALUES(12345.67)'); + +let rows: RowDataPacket[], fields: FieldPacket[]; +connection.execute( + 'SELECT f FROM t', + (err, _rows, _fields) => { + if (err) { + throw err; + } + rows = _rows; + fields = _fields; + connection.end(); + } +); + +process.on('exit', () => { + assert.deepEqual(rows, [{ f: '12345.6700' }]); + assert.equal(fields[0].name, 'f'); +}); diff --git a/test/esm/integration/connection/test-execute-nocolumndef.test.mts b/test/esm/integration/connection/test-execute-nocolumndef.test.mts new file mode 100644 index 0000000000..a6b0dd7569 --- /dev/null +++ b/test/esm/integration/connection/test-execute-nocolumndef.test.mts @@ -0,0 +1,230 @@ +// This file was modified by Oracle on June 2, 2021. +// The test has been updated to remove all expectations with regards to the +// "columnLength" metadata field. +// Modifications copyright (c) 2021, Oracle and/or its affiliates. + +import type { FieldPacket, RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +// @ts-expect-error: no typings available +import assert from 'assert-diff'; +import { createConnection } from '../../common.test.mjs'; + +// different error codes for PS, disabling for now +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test for planetscale'); + process.exit(0); +} + +const connection = createConnection(); + +// https://github.com/sidorares/node-mysql2/issues/130 +// https://github.com/sidorares/node-mysql2/issues/37 +// binary protocol examples where `prepare` returns no column definitions but execute() does return fields/rows + +let rows: RowDataPacket[]; +let fields: FieldPacket[]; + +connection.execute( + 'explain SELECT 1', + (err, _rows, _fields) => { + if (err) { + throw err; + } + + rows = _rows; + fields = _fields; + connection.end(); + } +); + +const expectedRows = [ + { + id: 1, + select_type: 'SIMPLE', + table: null, + type: null, + possible_keys: null, + key: null, + key_len: null, + ref: null, + rows: null, + Extra: 'No tables used', + partitions: null, + filtered: null, + }, +]; + +const expectedFields = [ + { + catalog: 'def', + schema: '', + name: 'id', + orgName: '', + table: '', + orgTable: '', + characterSet: 63, + encoding: 'binary', + type: 8, + flags: 161, + decimals: 0, + }, + { + catalog: 'def', + schema: '', + name: 'select_type', + orgName: '', + table: '', + orgTable: '', + characterSet: 224, + encoding: 'utf8', + type: 253, + flags: 1, + decimals: 31, + }, + { + catalog: 'def', + schema: '', + name: 'table', + orgName: '', + table: '', + orgTable: '', + characterSet: 224, + encoding: 'utf8', + type: 253, + flags: 0, + decimals: 31, + }, + { + catalog: 'def', + schema: '', + name: 'partitions', + orgName: '', + table: '', + orgTable: '', + characterSet: 224, + encoding: 'utf8', + type: 250, + flags: 0, + decimals: 31, + }, + { + catalog: 'def', + schema: '', + name: 'type', + orgName: '', + table: '', + orgTable: '', + characterSet: 224, + encoding: 'utf8', + type: 253, + flags: 0, + decimals: 31, + }, + { + catalog: 'def', + schema: '', + name: 'possible_keys', + orgName: '', + table: '', + orgTable: '', + characterSet: 224, + encoding: 'utf8', + type: 253, + flags: 0, + decimals: 31, + }, + { + catalog: 'def', + schema: '', + name: 'key', + orgName: '', + table: '', + orgTable: '', + characterSet: 224, + encoding: 'utf8', + type: 253, + flags: 0, + decimals: 31, + }, + { + catalog: 'def', + schema: '', + name: 'key_len', + orgName: '', + table: '', + orgTable: '', + characterSet: 224, + encoding: 'utf8', + type: 253, + flags: 0, + decimals: 31, + }, + { + catalog: 'def', + schema: '', + name: 'ref', + orgName: '', + table: '', + orgTable: '', + characterSet: 224, + encoding: 'utf8', + type: 253, + flags: 0, + decimals: 31, + }, + { + catalog: 'def', + schema: '', + name: 'rows', + orgName: '', + table: '', + orgTable: '', + characterSet: 63, + encoding: 'binary', + type: 8, + flags: 160, + decimals: 0, + }, + { + catalog: 'def', + schema: '', + name: 'filtered', + orgName: '', + table: '', + orgTable: '', + characterSet: 63, + encoding: 'binary', + type: 5, + flags: 128, + decimals: 2, + }, + { + catalog: 'def', + schema: '', + name: 'Extra', + orgName: '', + table: '', + orgTable: '', + characterSet: 224, + encoding: 'utf8', + type: 253, + flags: 1, + decimals: 31, + }, +]; + +process.on('exit', () => { + assert.deepEqual(rows, expectedRows); + fields.forEach((f, index) => { + // @ts-expect-error: TODO: implement typings + const fi = f.inspect(); + // "columnLength" is non-deterministic + delete fi.columnLength; + + assert.deepEqual( + Object.keys(fi).sort(), + Object.keys(expectedFields[index]).sort() + ); + assert.deepEqual(expectedFields[index], fi); + }); +}); diff --git a/test/esm/integration/connection/test-execute-null-bitmap.test.mts b/test/esm/integration/connection/test-execute-null-bitmap.test.mts new file mode 100644 index 0000000000..40e83365a1 --- /dev/null +++ b/test/esm/integration/connection/test-execute-null-bitmap.test.mts @@ -0,0 +1,29 @@ +import type { RowDataPacket } from '../../../../index.js'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +type TestRow = RowDataPacket & { t: number }; + +const connection = createConnection(); + +const params = [1, 2]; +let query = 'select ? + ?'; + +function dotest() { + connection.execute(`${query} as t`, params, (err, _rows) => { + assert.equal(err, null); + if (params.length < 50) { + assert.equal( + _rows[0].t, + params.reduce((x: number, y: number) => x + y) + ); + query += ' + ?'; + params.push(params.length); + dotest(); + } else { + connection.end(); + } + }); +} + +connection.query('SET GLOBAL max_prepared_stmt_count=300', dotest); diff --git a/test/esm/integration/connection/test-execute-order.test.mts b/test/esm/integration/connection/test-execute-order.test.mts new file mode 100644 index 0000000000..10a858a2c3 --- /dev/null +++ b/test/esm/integration/connection/test-execute-order.test.mts @@ -0,0 +1,24 @@ +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +const order: number[] = []; +connection.execute('select 1+2', (err) => { + assert.ifError(err); + order.push(0); +}); +connection.execute('select 2+2', (err) => { + assert.ifError(err); + order.push(1); +}); +connection.query('select 1+1', (err) => { + assert.ifError(err); + order.push(2); + connection.end(); +}); + +process.on('exit', () => { + assert.deepEqual(order, [0, 1, 2]); +}); diff --git a/test/esm/integration/connection/test-execute-signed.test.mts b/test/esm/integration/connection/test-execute-signed.test.mts new file mode 100644 index 0000000000..a489322354 --- /dev/null +++ b/test/esm/integration/connection/test-execute-signed.test.mts @@ -0,0 +1,44 @@ +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +type TestRow = RowDataPacket & { id: number; num: number; l: number }; + +const connection = createConnection(); + +let rows: TestRow[] | undefined = undefined; + +connection.query( + [ + 'CREATE TEMPORARY TABLE `test_table` (', + '`id` int(11) unsigned NOT NULL AUTO_INCREMENT,', + '`num` int(15),', + '`l` long,', + 'PRIMARY KEY (`id`)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8', + ].join('\n') +); + +connection.query('insert into test_table(num,l) values(?, 3)', [1]); +connection.query('insert into test_table(num,l) values(3-?, -10)', [5]); +connection.query( + 'insert into test_table(num,l) values(4+?, 4000000-?)', + [-5, 8000000] +); + +connection.execute('SELECT * from test_table', [], (err, _rows) => { + if (err) { + throw err; + } + rows = _rows; + connection.end(); +}); + +process.on('exit', () => { + assert.deepEqual(rows, [ + { id: 1, num: 1, l: 3 }, + { id: 2, num: -2, l: -10 }, + { id: 3, num: -1, l: -4000000 }, + ]); +}); diff --git a/test/esm/integration/connection/test-execute-type-casting.test.mts b/test/esm/integration/connection/test-execute-type-casting.test.mts new file mode 100644 index 0000000000..3241631957 --- /dev/null +++ b/test/esm/integration/connection/test-execute-type-casting.test.mts @@ -0,0 +1,99 @@ +import type { RowDataPacket } from '../../../../index.js'; +import { Buffer } from 'node:buffer'; +import process from 'node:process'; +import { assert, test } from 'poku'; +import { createConnection, useTestDb } from '../../common.test.mjs'; +import typeCastingTests from './type-casting-tests.test.mjs'; + +type TypeCastTest = { + type: string; + insert: string | number | Date | Buffer | null; + columnType: string; + expect?: unknown; + insertRaw?: string; + deep?: boolean; + columnName?: string; +}; + +test(async () => { + const connection = createConnection(); + + useTestDb(); + + connection.query('select 1', async (waitConnectErr) => { + assert.ifError(waitConnectErr); + const tests = (await typeCastingTests(connection)) as TypeCastTest[]; + + const table = 'type_casting'; + + const schema: string[] = []; + const inserts: string[] = []; + + tests.forEach((test, index) => { + const escaped = test.insertRaw || connection.escape(test.insert); + + test.columnName = `${test.type}_${index}`; + + schema.push(`\`${test.columnName}\` ${test.type},`); + inserts.push(`\`${test.columnName}\` = ${escaped}`); + }); + + const createTable = [ + `CREATE TEMPORARY TABLE \`${table}\` (`, + '`id` int(11) unsigned NOT NULL AUTO_INCREMENT,', + ] + .concat(schema) + .concat(['PRIMARY KEY (`id`)', ') ENGINE=InnoDB DEFAULT CHARSET=utf8']) + .join('\n'); + + connection.query(createTable); + + connection.query(`INSERT INTO ${table} SET${inserts.join(',\n')}`); + + let row: RowDataPacket | undefined; + connection.execute( + `SELECT * FROM ${table} WHERE id = ?;`, + [1], + (err, rows) => { + if (err) { + throw err; + } + + row = rows[0]; + connection.end(); + } + ); + + process.on('exit', () => { + tests.forEach((test) => { + let expected: unknown = test.expect || test.insert; + let got: unknown = row?.[test.columnName ?? '']; + let message: string; + + if (expected instanceof Date) { + assert.equal(got instanceof Date, true, test.type); + + expected = String(expected); + got = String(got); + } else if (Buffer.isBuffer(expected)) { + assert.equal(Buffer.isBuffer(got), true, test.type); + + expected = String(Array.prototype.slice.call(expected)); + got = String(Array.prototype.slice.call(got)); + } + + if (test.deep) { + message = `got: "${JSON.stringify(got)}" expected: "${JSON.stringify( + expected + )}" test: ${test.type}`; + assert.deepEqual(expected, got, message); + } else { + message = `got: "${got}" (${typeof got}) expected: "${expected}" (${typeof expected}) test: ${ + test.type + }`; + assert.strictEqual(expected, got, message); + } + }); + }); + }); +}); diff --git a/test/esm/integration/connection/test-insert-bigint-big-number-strings.test.mts b/test/esm/integration/connection/test-insert-bigint-big-number-strings.test.mts new file mode 100644 index 0000000000..9259366c01 --- /dev/null +++ b/test/esm/integration/connection/test-insert-bigint-big-number-strings.test.mts @@ -0,0 +1,72 @@ +import type { ResultSetHeader, RowDataPacket } from '../../../../index.js'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +type BigRow = RowDataPacket & { id: string; title: string }; + +const connection = createConnection({ + supportBigNumbers: true, + bigNumberStrings: true, +}); + +connection.query( + [ + 'CREATE TEMPORARY TABLE `bigs` (', + '`id` bigint NOT NULL AUTO_INCREMENT,', + '`title` varchar(255),', + 'PRIMARY KEY (`id`)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8', + ].join('\n') +); + +connection.query("INSERT INTO bigs SET title='test', id=123"); +connection.query( + "INSERT INTO bigs SET title='test1'", + (err, result) => { + if (err) { + throw err; + } + assert.strictEqual(result.insertId, 124); + // > 24 bits + connection.query("INSERT INTO bigs SET title='test', id=123456789"); + connection.query( + "INSERT INTO bigs SET title='test2'", + (_err, result) => { + assert.strictEqual(result.insertId, 123456790); + // big int + connection.query( + "INSERT INTO bigs SET title='test', id=9007199254740992" + ); + connection.query( + "INSERT INTO bigs SET title='test3'", + (_err, result) => { + assert.strictEqual(result.insertId, '9007199254740993'); + connection.query( + "INSERT INTO bigs SET title='test', id=90071992547409924" + ); + connection.query( + "INSERT INTO bigs SET title='test4'", + (_err, result) => { + assert.strictEqual(result.insertId, '90071992547409925'); + connection.query( + 'select * from bigs', + (_err, result) => { + assert.strictEqual(result[0].id, '123'); + assert.strictEqual(result[1].id, '124'); + assert.strictEqual(result[2].id, '123456789'); + assert.strictEqual(result[3].id, '123456790'); + assert.strictEqual(result[4].id, '9007199254740992'); + assert.strictEqual(result[5].id, '9007199254740993'); + assert.strictEqual(result[6].id, '90071992547409924'); + assert.strictEqual(result[7].id, '90071992547409925'); + connection.end(); + } + ); + } + ); + } + ); + } + ); + } +); diff --git a/test/esm/integration/connection/test-insert-bigint.test.mts b/test/esm/integration/connection/test-insert-bigint.test.mts new file mode 100644 index 0000000000..9049e430c1 --- /dev/null +++ b/test/esm/integration/connection/test-insert-bigint.test.mts @@ -0,0 +1,81 @@ +import type { ResultSetHeader, RowDataPacket } from '../../../../index.js'; +import Long from 'long'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +type BigRow = RowDataPacket & { id: number | string; title: string }; + +const connection = createConnection(); + +connection.query( + [ + 'CREATE TEMPORARY TABLE `bigs` (', + '`id` bigint NOT NULL AUTO_INCREMENT,', + '`title` varchar(255),', + 'PRIMARY KEY (`id`)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8', + ].join('\n') +); + +connection.query("INSERT INTO bigs SET title='test', id=123"); +connection.query( + "INSERT INTO bigs SET title='test1'", + (_err, result) => { + if (_err) { + throw _err; + } + assert.strictEqual(result.insertId, 124); + // > 24 bits + connection.query("INSERT INTO bigs SET title='test', id=123456789"); + connection.query( + "INSERT INTO bigs SET title='test2'", + (_err, result) => { + assert.strictEqual(result.insertId, 123456790); + // big int + connection.query( + "INSERT INTO bigs SET title='test', id=9007199254740992" + ); + connection.query( + "INSERT INTO bigs SET title='test3'", + (_err, result) => { + assert.strictEqual( + Long.fromString('9007199254740993').compare(result.insertId), + 0 + ); + connection.query( + "INSERT INTO bigs SET title='test', id=90071992547409924" + ); + connection.query( + "INSERT INTO bigs SET title='test4'", + (_err, result) => { + assert.strictEqual( + Long.fromString('90071992547409925').compare(result.insertId), + 0 + ); + connection.query( + { + sql: 'select * from bigs', + // @ts-expect-error: supportBigNumbers is not in QueryOptions typings + supportBigNumbers: true, + bigNumberString: false, + }, + (_err, result) => { + assert.strictEqual(result[0].id, 123); + assert.strictEqual(result[1].id, 124); + assert.strictEqual(result[2].id, 123456789); + assert.strictEqual(result[3].id, 123456790); + assert.strictEqual(result[4].id, 9007199254740992); + assert.strictEqual(result[5].id, '9007199254740993'); + assert.strictEqual(result[6].id, '90071992547409924'); + assert.strictEqual(result[7].id, '90071992547409925'); + connection.end(); + } + ); + } + ); + } + ); + } + ); + } +); diff --git a/test/esm/integration/connection/test-insert-json.test.mts b/test/esm/integration/connection/test-insert-json.test.mts new file mode 100644 index 0000000000..e88cf298e0 --- /dev/null +++ b/test/esm/integration/connection/test-insert-json.test.mts @@ -0,0 +1,39 @@ +/** + * Created by Elijah Melton on 2023.05.03 + * issue#1924: https://github.com/sidorares/node-mysql2/issues/1924 + */ + +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +type JsonRow = RowDataPacket & { data: { k: string } }; + +const connection = createConnection(); + +let result: JsonRow[] | undefined; +let errorCodeInvalidJSON: string | undefined; +let errorNumInvalidJSON: number | undefined; + +connection.query('CREATE TEMPORARY TABLE json_test (data JSON)'); +connection.query('INSERT INTO json_test VALUES (?)', ['{"k": "v"'], (err) => { + errorCodeInvalidJSON = err?.code; + errorNumInvalidJSON = err?.errno; +}); + +connection.query('INSERT INTO json_test VALUES (?)', ['{"k": "v"}'], (err) => { + if (err) throw err; +}); + +connection.query('SELECT * FROM json_test;', [], (err, res) => { + if (err) throw err; + result = res; + connection.end(); +}); + +process.on('exit', () => { + assert.equal(errorCodeInvalidJSON, 'ER_INVALID_JSON_TEXT'); + assert.equal(errorNumInvalidJSON, 3140); + assert.equal(result?.[0].data.k, 'v'); +}); diff --git a/test/esm/integration/connection/test-insert-large-blob.test.mts b/test/esm/integration/connection/test-insert-large-blob.test.mts new file mode 100644 index 0000000000..0cdfbfbc83 --- /dev/null +++ b/test/esm/integration/connection/test-insert-large-blob.test.mts @@ -0,0 +1,98 @@ +import type { ResultSetHeader, RowDataPacket } from '../../../../index.js'; +import { Buffer } from 'node:buffer'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +type BlobRow = RowDataPacket & { id: number; content: Buffer }; + +// intentionally disabled + +const _disabled = false as boolean; +if (_disabled) { + const connection = createConnection(); + + /* + connection.query('SELECT repeat("a", 60000000) as qqq', function (err, res) { + console.log(err); + console.log(err, res[0].qqq.length); + connection.end(); + }); + return; +*/ + + const table = 'insert_large_test'; + const length = 35777416; + const content = Buffer.allocUnsafe(length); // > 16 megabytes + const content1 = Buffer.allocUnsafe(length); // > 16 megabytes + + // this is to force compressed packed to be larger than uncompressed + for (let i = 0; i < content.length; ++i) { + content[i] = Math.floor(Math.random() * 256); + content1[i] = Math.floor(Math.random() * 256); + + // low entropy version, compressed < uncompressed + if (i < length / 2) { + content1[i] = 100; + } + } + + let result: ResultSetHeader; + let result2: BlobRow[]; + let result3: ResultSetHeader; + let result4: BlobRow[]; + + connection.query( + `SET GLOBAL max_allowed_packet=${length * 2 + 2000}`, + (err) => { + assert.ifError(err); + connection.end(); + const connection2 = createConnection(); + connection2.query( + [ + `CREATE TEMPORARY TABLE \`${table}\` (`, + '`id` int(11) unsigned NOT NULL AUTO_INCREMENT,', + '`content` longblob NOT NULL,', + 'PRIMARY KEY (`id`)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8', + ].join('\n') + ); + connection2.query( + `INSERT INTO ${table} (content) VALUES(?)`, + [content], + (err, _result) => { + assert.ifError(err); + result = _result; + connection2.query( + `SELECT * FROM ${table} WHERE id = ${result.insertId}`, + (_err, _result2) => { + result2 = _result2; + connection2.query( + `INSERT INTO ${table} (content) VALUES(?)`, + [content1], + (err, _result) => { + assert.ifError(err); + result3 = _result; + connection2.query( + `SELECT * FROM ${table} WHERE id = ${result3.insertId}`, + (err, _result) => { + assert.ifError(err); + result4 = _result; + connection2.end(); + } + ); + } + ); + } + ); + } + ); + } + ); + + process.on('exit', () => { + assert.equal(result2[0].id, String(result.insertId)); + assert.equal(result2[0].content.toString('hex'), content.toString('hex')); + assert.equal(result4[0].content.toString('hex'), content1.toString('hex')); + }); +} diff --git a/test/esm/integration/connection/test-insert-negative-ai.test.mts b/test/esm/integration/connection/test-insert-negative-ai.test.mts new file mode 100644 index 0000000000..c03134f7cf --- /dev/null +++ b/test/esm/integration/connection/test-insert-negative-ai.test.mts @@ -0,0 +1,58 @@ +import type { ResultSetHeader, RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +type NegAIRow = RowDataPacket & { id: number; title: string }; + +const connection = createConnection(); + +const testTable = 'neg-ai-test'; +const testData = 'test negative ai'; + +let selectResult: NegAIRow[]; +let insertResult: ResultSetHeader; + +const testNegativeAI = function (_err: Error | null) { + assert.ifError(_err); + // insert the negative AI + connection.query( + `INSERT INTO \`${testTable}\`` + + ` (id, title) values (-999, "${testData}")`, + (_err, result) => { + assert.ifError(_err); + insertResult = result; + + // select the row with negative AI + connection.query( + `SELECT * FROM \`${testTable}\`` + ` WHERE id = ${result.insertId}`, + (_err, result_) => { + assert.ifError(_err); + selectResult = result_; + connection.end(); + } + ); + } + ); +}; + +const prepareAndTest = function () { + connection.query( + `CREATE TEMPORARY TABLE \`${testTable}\` (` + + `\`id\` int(11) signed NOT NULL AUTO_INCREMENT,` + + `\`title\` varchar(255),` + + `PRIMARY KEY (\`id\`)` + + `) ENGINE=InnoDB DEFAULT CHARSET=utf8`, + testNegativeAI + ); +}; + +prepareAndTest(); + +process.on('exit', () => { + assert.strictEqual(insertResult.insertId, -999); + assert.strictEqual(selectResult.length, 1); + + assert.equal(selectResult[0].id, String(insertResult.insertId)); + assert.equal(selectResult[0].title, testData); +}); diff --git a/test/esm/integration/connection/test-insert-results.test.mts b/test/esm/integration/connection/test-insert-results.test.mts new file mode 100644 index 0000000000..fc3ccadc73 --- /dev/null +++ b/test/esm/integration/connection/test-insert-results.test.mts @@ -0,0 +1,50 @@ +import type { ResultSetHeader, RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +type InsertTestRow = RowDataPacket & { id: number; title: string }; + +const connection = createConnection(); + +// common.useTestDb(connection); + +const table = 'insert_test'; +// const text = "本日は晴天なり"; +const text = ' test test test '; +connection.query( + [ + `CREATE TEMPORARY TABLE \`${table}\` (`, + '`id` int(11) unsigned NOT NULL AUTO_INCREMENT,', + '`title` varchar(255),', + 'PRIMARY KEY (`id`)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8', + ].join('\n') +); + +let result: ResultSetHeader; +let result2: InsertTestRow[]; +connection.query( + `INSERT INTO ${table} SET title="${text}"`, + (err, _result) => { + if (err) { + throw err; + } + result = _result; + connection.query( + `SELECT * FROM ${table} WHERE id = ${result.insertId}`, + (_err, _result2) => { + result2 = _result2; + connection.end(); + } + ); + } +); + +process.on('exit', () => { + assert.strictEqual(result.insertId, 1); + assert.strictEqual(result2.length, 1); + // TODO: type conversions + assert.equal(result2[0].id, String(result.insertId)); + assert.equal(result2[0].title, text); +}); diff --git a/test/esm/integration/connection/test-invalid-date-result.test.mts b/test/esm/integration/connection/test-invalid-date-result.test.mts new file mode 100644 index 0000000000..1d8f8ed423 --- /dev/null +++ b/test/esm/integration/connection/test-invalid-date-result.test.mts @@ -0,0 +1,66 @@ +// This file was modified by Oracle on June 1, 2021. +// The test has been updated to be able to pass with different default +// strict modes used by different MySQL server versions. +// Modifications copyright (c) 2021, Oracle and/or its affiliates. + +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +type SqlModeRow = RowDataPacket & { value: string }; +type TimestampRow = RowDataPacket & { t: Date }; + +const connection = createConnection(); + +let rows: TimestampRow[] | undefined = undefined; + +// Disable NO_ZERO_DATE mode and NO_ZERO_IN_DATE mode to ensure the old +// behaviour. +const strictModes = ['NO_ZERO_DATE', 'NO_ZERO_IN_DATE']; + +connection.query( + 'SELECT variable_value as value FROM performance_schema.session_variables where variable_name = ?', + ['sql_mode'], + (err, _rows) => { + if (err) { + throw err; + } + + const deprecatedSqlMode = _rows[0].value + .split(',') + .filter((mode) => strictModes.indexOf(mode) === -1) + .join(','); + + connection.query(`SET sql_mode=?`, [deprecatedSqlMode], (err) => { + if (err) { + throw err; + } + + connection.execute( + 'SELECT TIMESTAMP(0000-00-00) t', + [], + (err, _rows) => { + if (err) { + throw err; + } + + rows = _rows; + connection.end(); + } + ); + }); + } +); + +function isInvalidTime(t: Date | undefined) { + return t ? isNaN(t.getTime()) : true; +} + +process.on('exit', () => { + assert.deepEqual( + Object.prototype.toString.call(rows?.[0].t), + '[object Date]' + ); + assert.deepEqual(isInvalidTime(rows?.[0].t), true); +}); diff --git a/test/esm/integration/connection/test-load-infile.test.mts b/test/esm/integration/connection/test-load-infile.test.mts new file mode 100644 index 0000000000..16feb055f3 --- /dev/null +++ b/test/esm/integration/connection/test-load-infile.test.mts @@ -0,0 +1,114 @@ +import type { + QueryError, + ResultSetHeader, + RowDataPacket, +} from '../../../../index.js'; +import fs from 'node:fs'; +import process from 'node:process'; +import { PassThrough } from 'node:stream'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test for planetscale'); + process.exit(0); +} + +const connection = createConnection(); + +const table = 'load_data_test'; +connection.query('SET GLOBAL local_infile = true', assert.ifError); +connection.query( + [ + `CREATE TEMPORARY TABLE \`${table}\` (`, + '`id` int(11) unsigned NOT NULL AUTO_INCREMENT,', + '`title` varchar(255),', + 'PRIMARY KEY (`id`)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8', + ].join('\n') +); + +const path = './test/fixtures/data.csv'; +const sql = + `LOAD DATA LOCAL INFILE ? INTO TABLE ${table} ` + + `FIELDS TERMINATED BY ? (id, title)`; + +let ok: ResultSetHeader; +connection.query( + { + sql, + values: [path, ','], + infileStreamFactory: () => fs.createReadStream(path), + }, + (err, _ok) => { + if (err) { + throw err; + } + ok = _ok; + } +); + +let rows: RowDataPacket[]; +connection.query(`SELECT * FROM ${table}`, (err, _rows) => { + if (err) { + throw err; + } + rows = _rows; +}); + +// Try to load a file that does not exist to see if we handle this properly +let loadErr: QueryError | null = null; +let loadResult: ResultSetHeader; +const badPath = '/does_not_exist.csv'; + +connection.query(sql, [badPath, ','], (err, result) => { + loadErr = err; + loadResult = result; +}); + +// test path mapping +const createMyStream = function () { + const myStream = new PassThrough(); + setTimeout(() => { + myStream.write('11,Hello World\n'); + myStream.write('21,One '); + myStream.write('more row\n'); + myStream.end(); + }, 1000); + return myStream; +}; + +let streamResult: ResultSetHeader; +connection.query( + { + sql: sql, + values: [badPath, ','], + infileStreamFactory: createMyStream, + }, + (err, result) => { + if (err) { + throw err; + } + streamResult = result; + connection.end(); + } +); + +process.on('exit', () => { + assert.equal(ok.affectedRows, 4); + assert.equal(rows.length, 4); + assert.equal(rows[0].id, 1); + assert.equal(rows[0].title.trim(), 'Hello World'); + + assert(loadErr, 'Expected LOAD DATA error'); + if (!loadErr) { + return; + } + assert.equal( + loadErr.message, + `As a result of LOCAL INFILE command server wants to read /does_not_exist.csv file, but as of v2.0 you must provide streamFactory option returning ReadStream.` + ); + assert.equal(loadResult.affectedRows, 0); + + assert.equal(streamResult.affectedRows, 2); +}); diff --git a/test/esm/integration/connection/test-multiple-results.test.mts b/test/esm/integration/connection/test-multiple-results.test.mts new file mode 100644 index 0000000000..9e3b12ddf5 --- /dev/null +++ b/test/esm/integration/connection/test-multiple-results.test.mts @@ -0,0 +1,237 @@ +// This file was modified by Oracle on June 2, 2021. +// The test has been updated to remove all expectations with regards to the +// "columnLength" metadata field. +// Modifications copyright (c) 2021, Oracle and/or its affiliates. + +import process from 'node:process'; +// @ts-expect-error: no typings available +import assert from 'assert-diff'; +import { createConnection } from '../../common.test.mjs'; + +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test for planetscale'); + process.exit(0); +} + +const mysql = createConnection({ + multipleStatements: true, +}); +mysql.query('CREATE TEMPORARY TABLE no_rows (test int)'); +mysql.query('CREATE TEMPORARY TABLE some_rows (test int)'); +mysql.query('INSERT INTO some_rows values(0)'); +mysql.query('INSERT INTO some_rows values(42)'); +mysql.query('INSERT INTO some_rows values(314149)'); + +const clone = function (obj: T): T { + return JSON.parse(JSON.stringify(obj)) as T; +}; + +const rs1 = { + affectedRows: 0, + fieldCount: 0, + insertId: 0, + serverStatus: 10, + warningStatus: 0, + info: '', + changedRows: 0, +}; +const rs2 = clone(rs1); +rs2.serverStatus = 2; + +const twoInsertResult = [[rs1, rs2], [undefined, undefined], 2]; +const select1 = [{ 1: '1' }]; +const select2 = [{ 2: '2' }]; +const fields1 = [ + { + catalog: 'def', + characterSet: 63, + encoding: 'binary', + type: 8, + decimals: 0, + flags: 129, + name: '1', + orgName: '', + orgTable: '', + schema: '', + table: '', + }, +]; +const nr_fields = [ + { + catalog: 'def', + characterSet: 63, + encoding: 'binary', + type: 3, + decimals: 0, + flags: 0, + name: 'test', + orgName: 'test', + orgTable: 'no_rows', + schema: mysql.config.database, + table: 'no_rows', + }, +]; +const sr_fields = clone(nr_fields); +sr_fields[0].orgTable = 'some_rows'; +sr_fields[0].table = 'some_rows'; +const select3 = [{ test: 0 }, { test: 42 }, { test: 314149 }]; + +const fields2 = clone(fields1); +fields2[0].name = '2'; + +const tests: [string, unknown[]][] = [ + ['select * from some_rows', [select3, sr_fields, 1]], // select 3 rows + [ + 'SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT; SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS;', + twoInsertResult, + ], + [ + '/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;', + twoInsertResult, + ], // issue #26 + ['set @a = 1', [rs2, undefined, 1]], // one insert result + ['set @a = 1; set @b = 2', twoInsertResult], + ['select 1; select 2', [[select1, select2], [fields1, fields2], 2]], + ['set @a = 1; select 1', [[rs1, select1], [undefined, fields1], 2]], + ['select 1; set @a = 1', [[select1, rs2], [fields1, undefined], 2]], + ['select * from no_rows', [[], nr_fields, 1]], // select 0 rows" + ['set @a = 1; select * from no_rows', [[rs1, []], [undefined, nr_fields], 2]], // insert + select 0 rows + ['select * from no_rows; set @a = 1', [[[], rs2], [nr_fields, undefined], 2]], // select 0 rows + insert + [ + 'set @a = 1; select * from some_rows', + [[rs1, select3], [undefined, sr_fields], 2], + ], // insert + select 3 rows + [ + 'select * from some_rows; set @a = 1', + [[select3, rs2], [sr_fields, undefined], 2], + ], // select 3 rows + insert +]; + +const hasConstructorName = ( + value: unknown +): value is { constructor: { name: string } } => { + if (typeof value !== 'object' || value === null) { + return false; + } + + const candidate = value as { constructor?: { name?: unknown } }; + return typeof candidate.constructor?.name === 'string'; +}; + +// TODO: tests with error in the query with different index +// TODO: multiple results from single query + +function do_test(testIndex: number) { + const entry = tests[testIndex]; + const sql = entry[0]; + const expectation = entry[1]; + mysql.query(sql, (err, _rows, _columns) => { + if (err) { + console.log(err); + process.exit(-1); + } + + const rows = _rows; + let _numResults = 0; + if ( + hasConstructorName(rows) && + rows.constructor.name === 'ResultSetHeader' + ) { + _numResults = 1; + } else if (Array.isArray(rows) && rows.length === 0) { + // empty select + _numResults = 1; + } else if (Array.isArray(rows) && rows.length > 0) { + _numResults = 1; + const first = rows[0]; + if ( + Array.isArray(first) || + (hasConstructorName(first) && + first.constructor.name === 'ResultSetHeader') + ) { + _numResults = rows.length; + } + } + + const arrOrColumn = function (c: unknown): unknown { + if (Array.isArray(c)) { + return c.map(arrOrColumn); + } + + if (typeof c === 'undefined') { + return void 0; + } + + // @ts-expect-error: internal access + const column = c.inspect() as Record; + // "columnLength" is non-deterministic and the display width for integer + // data types was deprecated on MySQL 8.0.17. + // https://dev.mysql.com/doc/refman/8.0/en/numeric-type-syntax.html + delete column.columnLength; + + return column; + }; + + assert.deepEqual(expectation, [_rows, arrOrColumn(_columns), _numResults]); + + const q = mysql.query(sql); + let resIndex = 0; + let rowIndex = 0; + + let fieldIndex = -1; + + const multiRows: unknown[] = Array.isArray(_rows) ? _rows : [_rows]; + + function checkRow(row: { + constructor: { name: string }; + [key: string]: unknown; + }) { + const index = fieldIndex; + if (_numResults === 1) { + assert.equal(fieldIndex, 0); + if (row.constructor.name === 'ResultSetHeader') { + assert.deepEqual(_rows, row); + } else { + assert.deepEqual(multiRows[rowIndex], row); + } + } else { + if (resIndex !== index) { + rowIndex = 0; + resIndex = index; + } + if (row.constructor.name === 'ResultSetHeader') { + assert.deepEqual(multiRows[index], row); + } else { + const resultRows = multiRows[index]; + if (Array.isArray(resultRows)) { + assert.deepEqual(resultRows[rowIndex], row); + } + } + } + rowIndex++; + } + + function checkFields(fields: unknown) { + fieldIndex++; + if (_numResults === 1) { + assert.equal(fieldIndex, 0); + assert.deepEqual(arrOrColumn(_columns), arrOrColumn(fields)); + } else { + assert.deepEqual( + arrOrColumn(_columns[fieldIndex]), + arrOrColumn(fields) + ); + } + } + q.on('result', checkRow); + q.on('fields', checkFields); + q.on('end', () => { + if (testIndex + 1 < tests.length) { + do_test(testIndex + 1); + } else { + mysql.end(); + } + }); + }); +} +do_test(0); diff --git a/test/esm/integration/connection/test-named-placeholders.test.mts b/test/esm/integration/connection/test-named-placeholders.test.mts new file mode 100644 index 0000000000..bca0dd37a7 --- /dev/null +++ b/test/esm/integration/connection/test-named-placeholders.test.mts @@ -0,0 +1,105 @@ +import type { Pool, RowDataPacket } from '../../../../index.js'; +import { assert } from 'poku'; +import { createConnection, createPool } from '../../common.test.mjs'; + +type SumRow = RowDataPacket & { sum: number }; + +const connection = createConnection(); + +connection.query( + [ + 'CREATE TEMPORARY TABLE `test_table` (', + '`id` int(11) unsigned NOT NULL AUTO_INCREMENT,', + '`num1` int(15),', + '`num2` int(15),', + 'PRIMARY KEY (`id`)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8', + ].join('\n') +); + +connection.query('insert into test_table(num1,num2) values(?, 3)', [1]); +connection.query('insert into test_table(num1,num2) values(3-?, -10)', [5]); +connection.query( + 'insert into test_table(num1,num2) values(4+?, 4000000-?)', + [-5, 8000000] +); +connection.query( + 'insert into test_table(num1,num2) values(?, ?)', + [-5, 8000000] +); + +connection.config.namedPlaceholders = true; +const cmd = connection.execute( + 'SELECT * from test_table where num1 < :numParam and num2 > :lParam', + { lParam: 100, numParam: 2 }, + (err, rows) => { + if (err) { + throw err; + } + assert.deepEqual(rows, [{ id: 4, num1: -5, num2: 8000000 }]); + } +); +assert.equal(cmd.sql, 'SELECT * from test_table where num1 < ? and num2 > ?'); +// @ts-expect-error: TODO: implement typings for Query.values +assert.deepEqual(cmd.values, [2, 100]); + +connection.execute('SELECT :a + :a as sum', { a: 2 }, (err, rows) => { + if (err) { + throw err; + } + assert.deepEqual(rows, [{ sum: 4 }]); +}); + +const qCmd = connection.query( + 'SELECT * from test_table where num1 < :numParam and num2 > :lParam', + { lParam: 100, numParam: 2 }, + (err, rows) => { + if (err) { + throw err; + } + assert.deepEqual(rows, [{ id: 4, num1: -5, num2: 8000000 }]); + } +); +assert.equal( + qCmd.sql, + 'SELECT * from test_table where num1 < 2 and num2 > 100' +); +// @ts-expect-error: TODO: implement typings for Query.values +assert.deepEqual(qCmd.values, [2, 100]); + +connection.query('SELECT :a + :a as sum', { a: 2 }, (err, rows) => { + if (err) { + throw err; + } + assert.deepEqual(rows, [{ sum: 4 }]); + connection.end(); +}); + +const namedSql = connection.format( + 'SELECT * from test_table where num1 < :numParam and num2 > :lParam', + { lParam: 100, numParam: 2 } +); +assert.equal( + namedSql, + 'SELECT * from test_table where num1 < 2 and num2 > 100' +); + +const unnamedSql = connection.format( + 'SELECT * from test_table where num1 < ? and num2 > ?', + [2, 100] +); +assert.equal( + unnamedSql, + 'SELECT * from test_table where num1 < 2 and num2 > 100' +); + +const pool: Pool = createPool(); +// @ts-expect-error: TODO: implement typings +pool.config.connectionConfig.namedPlaceholders = true; +pool.query('SELECT :a + :a as sum', { a: 2 }, (err, rows) => { + pool.end(); + if (err) { + throw err; + } + assert.deepEqual(rows, [{ sum: 4 }]); +}); diff --git a/test/esm/integration/connection/test-nested-tables-query.test.mts b/test/esm/integration/connection/test-nested-tables-query.test.mts new file mode 100644 index 0000000000..fdcaedc538 --- /dev/null +++ b/test/esm/integration/connection/test-nested-tables-query.test.mts @@ -0,0 +1,172 @@ +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection, useTestDb } from '../../common.test.mjs'; + +const connection = createConnection(); + +useTestDb(); + +const table = 'nested_test'; +connection.query( + [ + `CREATE TEMPORARY TABLE \`${table}\` (`, + '`id` int(11) unsigned NOT NULL AUTO_INCREMENT,', + '`title` varchar(255),', + 'PRIMARY KEY (`id`)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8', + ].join('\n') +); +connection.query( + [ + `CREATE TEMPORARY TABLE \`${table}1\` (`, + '`id` int(11) unsigned NOT NULL AUTO_INCREMENT,', + '`title` varchar(255),', + 'PRIMARY KEY (`id`)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8', + ].join('\n') +); + +connection.query(`INSERT INTO ${table} SET ?`, { title: 'test' }); +connection.query(`INSERT INTO ${table}1 SET ?`, { title: 'test1' }); + +const options1 = { + nestTables: true, + sql: `SELECT * FROM ${table}`, +}; +const options2 = { + nestTables: '_', + sql: `SELECT * FROM ${table}`, +}; +const options3 = { + rowsAsArray: true, + sql: `SELECT * FROM ${table}`, +}; +const options4 = { + nestTables: true, + sql: `SELECT notNested.id, notNested.title, nested.title FROM ${table} notNested LEFT JOIN ${table}1 nested ON notNested.id = nested.id`, +}; +const options5 = { + nestTables: true, + sql: `SELECT notNested.id, notNested.title, nested2.title FROM ${table} notNested LEFT JOIN ${table}1 nested2 ON notNested.id = nested2.id`, +}; + +let rows1: RowDataPacket[], + rows2: RowDataPacket[], + rows3: RowDataPacket[], + rows4: RowDataPacket[], + rows5: RowDataPacket[], + rows1e: RowDataPacket[], + rows2e: RowDataPacket[], + rows3e: RowDataPacket[]; + +connection.query(options1, (err, _rows) => { + if (err) { + throw err; + } + + rows1 = _rows; +}); + +connection.query(options2, (err, _rows) => { + if (err) { + throw err; + } + + rows2 = _rows; +}); + +connection.query(options3, (err, _rows) => { + if (err) { + throw err; + } + + rows3 = _rows; +}); + +connection.query(options4, (err, _rows) => { + if (err) { + throw err; + } + + rows4 = _rows; +}); + +connection.query(options5, (err, _rows) => { + if (err) { + throw err; + } + + rows5 = _rows; +}); + +connection.execute(options1, (err, _rows) => { + if (err) { + throw err; + } + + rows1e = _rows; +}); + +connection.execute(options2, (err, _rows) => { + if (err) { + throw err; + } + + rows2e = _rows; +}); + +connection.execute(options3, (err, _rows) => { + if (err) { + throw err; + } + + rows3e = _rows; + connection.end(); +}); + +process.on('exit', () => { + assert.equal(rows1.length, 1, 'First row length'); + assert.equal(rows1[0].nested_test.id, 1, 'First row nested id'); + assert.equal(rows1[0].nested_test.title, 'test', 'First row nested title'); + assert.equal(rows2.length, 1, 'Second row length'); + assert.equal(rows2[0].nested_test_id, 1, 'Second row nested id'); + assert.equal(rows2[0].nested_test_title, 'test', 'Second row nested title'); + + assert.equal(Array.isArray(rows3[0]), true, 'Third row type'); + assert.equal(rows3[0][0], 1, 'Third row value 1'); + assert.equal(rows3[0][1], 'test', 'Third row value 2'); + + assert.equal(rows4.length, 1, 'Fourth row length'); + assert.deepEqual( + rows4[0], + { + nested: { + title: 'test1', + }, + notNested: { + id: 1, + title: 'test', + }, + }, + 'Fourth row value' + ); + assert.equal(rows5.length, 1, 'Fifth row length'); + assert.deepEqual( + rows5[0], + { + nested2: { + title: 'test1', + }, + notNested: { + id: 1, + title: 'test', + }, + }, + 'Fifth row value' + ); + + assert.deepEqual(rows1, rows1e, 'Compare rows1 with rows1e'); + assert.deepEqual(rows2, rows2e, 'Compare rows2 with rows2e'); + assert.deepEqual(rows3, rows3e, 'Compare rows3 with rows3e'); +}); diff --git a/test/esm/integration/connection/test-null-buffer.test.mts b/test/esm/integration/connection/test-null-buffer.test.mts new file mode 100644 index 0000000000..7f0d160a01 --- /dev/null +++ b/test/esm/integration/connection/test-null-buffer.test.mts @@ -0,0 +1,37 @@ +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +let rowsTextProtocol: RowDataPacket[]; +let rowsBinaryProtocol: RowDataPacket[]; + +connection.query('CREATE TEMPORARY TABLE binary_table (stuff BINARY(16));'); +connection.query('INSERT INTO binary_table VALUES(null)'); + +connection.query( + 'SELECT * from binary_table', + (err, _rows) => { + if (err) { + throw err; + } + rowsTextProtocol = _rows; + connection.execute( + 'SELECT * from binary_table', + (err, _rows) => { + if (err) { + throw err; + } + rowsBinaryProtocol = _rows; + connection.end(); + } + ); + } +); + +process.on('exit', () => { + assert.deepEqual(rowsTextProtocol[0], { stuff: null }); + assert.deepEqual(rowsBinaryProtocol[0], { stuff: null }); +}); diff --git a/test/esm/integration/connection/test-null-double.test.mts b/test/esm/integration/connection/test-null-double.test.mts new file mode 100644 index 0000000000..abe43ab4e2 --- /dev/null +++ b/test/esm/integration/connection/test-null-double.test.mts @@ -0,0 +1,25 @@ +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +let rows: RowDataPacket[]; + +connection.query('CREATE TEMPORARY TABLE t (i int)'); +connection.query('INSERT INTO t VALUES(null)'); +connection.query('INSERT INTO t VALUES(123)'); + +connection.query('SELECT * from t', (err, _rows) => { + if (err) { + throw err; + } + rows = _rows; + connection.end(); +}); + +process.on('exit', () => { + assert.deepEqual(rows[0], { i: null }); + assert.deepEqual(rows[1], { i: 123 }); +}); diff --git a/test/esm/integration/connection/test-null-int.test.mts b/test/esm/integration/connection/test-null-int.test.mts new file mode 100644 index 0000000000..abe43ab4e2 --- /dev/null +++ b/test/esm/integration/connection/test-null-int.test.mts @@ -0,0 +1,25 @@ +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +let rows: RowDataPacket[]; + +connection.query('CREATE TEMPORARY TABLE t (i int)'); +connection.query('INSERT INTO t VALUES(null)'); +connection.query('INSERT INTO t VALUES(123)'); + +connection.query('SELECT * from t', (err, _rows) => { + if (err) { + throw err; + } + rows = _rows; + connection.end(); +}); + +process.on('exit', () => { + assert.deepEqual(rows[0], { i: null }); + assert.deepEqual(rows[1], { i: 123 }); +}); diff --git a/test/esm/integration/connection/test-null.test.mts b/test/esm/integration/connection/test-null.test.mts new file mode 100644 index 0000000000..a608fb09d1 --- /dev/null +++ b/test/esm/integration/connection/test-null.test.mts @@ -0,0 +1,36 @@ +import type { FieldPacket, RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +let rows: RowDataPacket[], rows1: RowDataPacket[]; +let fields1: FieldPacket[]; + +connection.query('CREATE TEMPORARY TABLE t (i int)'); +connection.query('INSERT INTO t VALUES(null)'); +connection.query( + 'SELECT cast(NULL AS CHAR) as cast_result', + (err, _rows) => { + if (err) { + throw err; + } + rows = _rows; + } +); +connection.query('SELECT * from t', (err, _rows, _fields) => { + if (err) { + throw err; + } + rows1 = _rows; + fields1 = _fields; + connection.end(); +}); + +process.on('exit', () => { + assert.deepEqual(rows, [{ cast_result: null }]); + // assert.equal(fields[0].columnType, 253); // depeding on the server type could be 253 or 3, disabling this check for now + assert.deepEqual(rows1, [{ i: null }]); + assert.equal(fields1[0].columnType, 3); +}); diff --git a/test/esm/integration/connection/test-parameters-questionmark.test.mts b/test/esm/integration/connection/test-parameters-questionmark.test.mts new file mode 100644 index 0000000000..5bee4910eb --- /dev/null +++ b/test/esm/integration/connection/test-parameters-questionmark.test.mts @@ -0,0 +1,34 @@ +import type { Pool, RowDataPacket } from '../../../../index.js'; +import { assert } from 'poku'; +import { createPool } from '../../common.test.mjs'; + +type TestRow = RowDataPacket & { str: string }; + +const pool: Pool = createPool(); +pool.config.connectionLimit = 1; + +pool.query( + [ + 'CREATE TEMPORARY TABLE `test_table` (', + '`id` int(11) unsigned NOT NULL AUTO_INCREMENT,', + '`str` varchar(64),', + 'PRIMARY KEY (`id`)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8', + ].join('\n') +); +pool.query('insert into test_table(str) values(?)', ['abc?']); +pool.query('UPDATE test_table SET str = ? WHERE id = ?', [ + 'should not change ?', + 1, +]); +pool.query( + 'SELECT str FROM test_table WHERE id = ?', + [1], + (err, rows) => { + pool.end(); + if (err) { + throw err; + } + assert.deepEqual(rows, [{ str: 'should not change ?' }]); + } +); diff --git a/test/esm/integration/connection/test-prepare-and-close.test.mts b/test/esm/integration/connection/test-prepare-and-close.test.mts new file mode 100644 index 0000000000..12f7576411 --- /dev/null +++ b/test/esm/integration/connection/test-prepare-and-close.test.mts @@ -0,0 +1,32 @@ +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +const max = 500; +const start = process.hrtime(); +function prepare(i: number) { + connection.prepare(`select 1+${i}`, (err, stmt) => { + assert.ifError(err); + stmt.close(); + if (!err) { + if (i > max) { + const end = process.hrtime(start); + const ns = end[0] * 1e9 + end[1]; + console.log(`${(max * 1e9) / ns} prepares/sec`); + connection.end(); + return; + } + setTimeout(() => { + prepare(i + 1); + }, 2); + return; + } + assert(0, 'Error in prepare!'); + }); +} +connection.query('SET GLOBAL max_prepared_stmt_count=10', (err) => { + assert.ifError(err); + prepare(1); +}); diff --git a/test/esm/integration/connection/test-prepare-simple.test.mts b/test/esm/integration/connection/test-prepare-simple.test.mts new file mode 100644 index 0000000000..471917cef7 --- /dev/null +++ b/test/esm/integration/connection/test-prepare-simple.test.mts @@ -0,0 +1,41 @@ +import type { PrepareStatementInfo } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +let _stmt1: PrepareStatementInfo | null = null; +const query1 = 'select 1 + ? + ? as test'; +const query2 = 'select 1 + 1'; // no parameters +const query3 = 'create temporary table aaa(i int);'; // no parameters, no result columns + +connection.prepare(query1, (err1, stmt1) => { + assert.ifError(err1); + _stmt1 = stmt1; + _stmt1.close(); + connection.prepare(query2, (err2, stmt2) => { + assert.ifError(err2); + connection.prepare(query3, (err3, stmt3) => { + assert.ifError(err3); + stmt2.close(); + stmt3.close(); + connection.end(); + }); + }); +}); + +process.on('exit', () => { + assert(_stmt1, 'Expected prepared statement'); + if (!_stmt1) { + return; + } + // @ts-expect-error: TODO: implement typings + assert.equal(_stmt1.query, query1); + // @ts-expect-error: TODO: implement typings + assert(_stmt1.id >= 0); + // @ts-expect-error: TODO: implement typings + assert.equal(_stmt1.columns.length, 1); + // @ts-expect-error: TODO: implement typings + assert.equal(_stmt1.parameters.length, 2); +}); diff --git a/test/esm/integration/connection/test-prepare-then-execute.test.mts b/test/esm/integration/connection/test-prepare-then-execute.test.mts new file mode 100644 index 0000000000..caae5fe69e --- /dev/null +++ b/test/esm/integration/connection/test-prepare-then-execute.test.mts @@ -0,0 +1,43 @@ +import type { + FieldPacket, + PrepareStatementInfo, + RowDataPacket, +} from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +let _stmt: PrepareStatementInfo | null = null; +let _columns: FieldPacket[] | null = null; +let _rows: RowDataPacket[] | null = null; + +connection.prepare('select 1 + ? + ? as test', (err, stmt) => { + if (err) { + throw err; + } + _stmt = stmt; + stmt.execute([111, 123], (err, rows, columns) => { + if (err) { + throw err; + } + _columns = columns; + _rows = rows; + connection.end(); + }); +}); + +process.on('exit', () => { + assert(_stmt, 'Expected prepared statement'); + assert(_columns, 'Expected statement metadata'); + if (!_stmt || !_columns) { + return; + } + // @ts-expect-error: TODO: implement typings + assert.equal(_stmt.columns.length, 1); + // @ts-expect-error: TODO: implement typings + assert.equal(_stmt.parameters.length, 2); + assert.deepEqual(_rows, [{ test: 235 }]); + assert.equal(_columns[0].name, 'test'); +}); diff --git a/test/esm/integration/connection/test-protocol-errors.test.mts b/test/esm/integration/connection/test-protocol-errors.test.mts new file mode 100644 index 0000000000..89b9173849 --- /dev/null +++ b/test/esm/integration/connection/test-protocol-errors.test.mts @@ -0,0 +1,86 @@ +// This file was modified by Oracle on January 21, 2021. +// The connection with the mock server needs to happen in the same host where +// the tests are running in order to avoid connecting a potential MySQL server +// instance running in the host identified by the MYSQL_HOST environment +// variable. +// Modifications copyright (c) 2021, Oracle and/or its affiliates. + +import type { FieldPacket, RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection, createServer } from '../../common.test.mjs'; + +// The process is not terminated in Deno +if (typeof Deno !== 'undefined') process.exit(0); + +let fields: FieldPacket[] | undefined; +let error: (Error & { fatal?: boolean; code?: string }) | undefined; +const query = 'SELECT 1'; +let rows: RowDataPacket[] | undefined; + +const server = createServer( + () => { + const connection = createConnection({ + // The mock server is running on the same host machine. + // We need to explicitly define the host to avoid connecting to a potential + // different host provided via MYSQL_HOST that identifies a real MySQL + // server instance. + host: 'localhost', + // @ts-expect-error: internal access + port: server._port, + // @ts-expect-error: TODO: implement typings + ssl: false, + }); + connection.query(query, (err, _rows, _fields) => { + if (err) { + throw err; + } + rows = _rows; + fields = _fields; + }); + + connection.on('error', (err) => { + error = err; + // @ts-expect-error: internal access + if (server._server._handle) { + // @ts-expect-error: TODO: implement typings + server.close(); + } + }); + }, + (conn) => { + conn.on('query', () => { + conn.writeTextResult( + [{ 1: '1' }], + [ + { + catalog: 'def', + schema: '', + table: '', + orgTable: '', + name: '1', + orgName: '', + characterSet: 63, + columnLength: 1, + columnType: 8, + flags: 129, + decimals: 0, + }, + ] + ); + // this is extra (incorrect) packet - client should emit error on receiving it + conn.writeOk(); + }); + } +); + +process.on('exit', () => { + assert.deepEqual(rows, [{ 1: 1 }]); + assert.equal(fields?.[0].name, '1'); + assert.equal( + error?.message, + 'Unexpected packet while no commands in the queue' + ); + assert.equal(error?.fatal, true); + assert.equal(error?.code, 'PROTOCOL_UNEXPECTED_PACKET'); +}); diff --git a/test/esm/integration/connection/test-query-timeout.test.mts b/test/esm/integration/connection/test-query-timeout.test.mts new file mode 100644 index 0000000000..3bc475aba3 --- /dev/null +++ b/test/esm/integration/connection/test-query-timeout.test.mts @@ -0,0 +1,101 @@ +import assert from 'node:assert'; +import process from 'node:process'; +import portfinder from 'portfinder'; +import mysql from '../../../../index.js'; +import { createConnection } from '../../common.test.mjs'; + +// The process is not terminated in Deno +if (typeof Deno !== 'undefined') process.exit(0); + +const connection = createConnection({ debug: false }); + +connection.query({ sql: 'SELECT sleep(3) as a', timeout: 500 }, (err, res) => { + assert.equal(res, null); + assert.ok(err); + assert.equal(err.code, 'PROTOCOL_SEQUENCE_TIMEOUT'); + assert.equal(err.message, 'Query inactivity timeout'); +}); + +connection.query( + { sql: 'SELECT sleep(1) as a', timeout: 5000 }, + (_err, res) => { + assert.deepEqual(res, [{ a: 0 }]); + } +); + +connection.query('SELECT sleep(1) as a', (_err, res) => { + assert.deepEqual(res, [{ a: 0 }]); +}); + +connection.execute( + { sql: 'SELECT sleep(3) as a', timeout: 500 }, + (err, res) => { + assert.equal(res, null); + assert.ok(err); + assert.equal(err.code, 'PROTOCOL_SEQUENCE_TIMEOUT'); + assert.equal(err.message, 'Query inactivity timeout'); + } +); + +connection.execute( + { sql: 'SELECT sleep(1) as a', timeout: 5000 }, + (_err, res) => { + assert.deepEqual(res, [{ a: 0 }]); + } +); + +connection.query( + { sql: 'select 1 from non_existing_table', timeout: 500 }, + (err, res) => { + assert.equal(res, null); + assert.ok(err); + assert.equal(err.code, 'ER_NO_SUCH_TABLE'); + } +); + +connection.execute('SELECT sleep(1) as a', (_err, res) => { + assert.deepEqual(res, [{ a: 0 }]); + connection.end(); +}); + +/** + * if connect timeout + * we should return connect timeout error instead of query timeout error + */ +portfinder.getPort((_err, port) => { + // @ts-expect-error: TODO: implement typings + const server = mysql.createServer(); + server.on('connection', () => { + // Let connection time out + }); + server.listen(port); + + const connectionTimeout = mysql.createConnection({ + host: 'localhost', + port: port, + connectTimeout: 1000, + }); + + // return connect timeout error first + connectionTimeout.query( + { sql: 'SELECT sleep(3) as a', timeout: 50 }, + (err, res) => { + console.log('ok'); + assert.equal(res, null); + assert.ok(err); + assert.equal(err.code, 'ETIMEDOUT'); + assert.equal(err.message, 'connect ETIMEDOUT'); + connectionTimeout.destroy(); + // @ts-expect-error: internal access + server._server.close(); + } + ); +}); + +process.on('uncaughtException', (err: NodeJS.ErrnoException) => { + assert.equal( + err.message, + 'Connection lost: The server closed the connection.' + ); + assert.equal(err.code, 'PROTOCOL_CONNECTION_LOST'); +}); diff --git a/test/esm/integration/connection/test-query-zero.test.mts b/test/esm/integration/connection/test-query-zero.test.mts new file mode 100644 index 0000000000..425ebd3580 --- /dev/null +++ b/test/esm/integration/connection/test-query-zero.test.mts @@ -0,0 +1,19 @@ +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +let rows: RowDataPacket[]; +connection.query('SELECT ? AS result', 0, (err, _rows) => { + if (err) { + throw err; + } + rows = _rows; + connection.end(); +}); + +process.on('exit', () => { + assert.deepEqual(rows, [{ result: 0 }]); +}); diff --git a/test/esm/integration/connection/test-quit.test.mts b/test/esm/integration/connection/test-quit.test.mts new file mode 100644 index 0000000000..3e5879b326 --- /dev/null +++ b/test/esm/integration/connection/test-quit.test.mts @@ -0,0 +1,82 @@ +// This file was modified by Oracle on January 21, 2021. +// The connection with the mock server needs to happen in the same host where +// the tests are running in order to avoid connecting a potential MySQL server +// instance running in the host identified by the MYSQL_HOST environment +// variable. +// Modifications copyright (c) 2021, Oracle and/or its affiliates. + +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection, createServer } from '../../common.test.mjs'; + +// The process is not terminated in Deno +if (typeof Deno !== 'undefined') process.exit(0); + +let quitReceived = false; +const queryCli = 'SELECT 1'; +let queryServ: string; +let rows: Array>; +let fields: Array<{ name: string }>; +const server = createServer( + () => { + const connection = createConnection({ + // The mock server is running on the same host machine. + // We need to explicitly define the host to avoid connecting to a potential + // different host provided via MYSQL_HOST that identifies a real MySQL + // server instance. + host: 'localhost', + // @ts-expect-error: internal access + port: server._port, + ssl: undefined, + }); + + connection.query(queryCli, (err, _rows, _fields) => { + if (err) { + throw err; + } + rows = _rows as Array>; + fields = _fields as Array<{ name: string }>; + + connection.end(); + }); + }, + (conn) => { + conn.on('quit', () => { + // COM_QUIT + quitReceived = true; + // @ts-expect-error: TODO: implement typings + conn.stream.end(); + // @ts-expect-error: TODO: implement typings + server.close(); + }); + + conn.on('query', (q: string) => { + queryServ = q; + conn.writeTextResult( + [{ 1: '1' }], + [ + { + catalog: 'def', + schema: '', + table: '', + orgTable: '', + name: '1', + orgName: '', + characterSet: 63, + columnLength: 1, + columnType: 8, + flags: 129, + decimals: 0, + }, + ] + ); + }); + } +); + +process.on('exit', () => { + assert.deepEqual(rows, [{ 1: 1 }]); + assert.equal(fields[0].name, '1'); + assert.equal(quitReceived, true); + assert.equal(queryCli, queryServ); +}); diff --git a/test/esm/integration/connection/test-select-1.test.mts b/test/esm/integration/connection/test-select-1.test.mts new file mode 100644 index 0000000000..58a54cf3e0 --- /dev/null +++ b/test/esm/integration/connection/test-select-1.test.mts @@ -0,0 +1,22 @@ +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +connection.query('SELECT 1 as result', (err, rows, fields) => { + assert.ifError(err); + assert.deepEqual(rows, [{ result: 1 }]); + assert.equal(fields[0].name, 'result'); + + connection.execute('SELECT 1 as result', (err, rows, fields) => { + assert.ifError(err); + assert.deepEqual(rows, [{ result: 1 }]); + assert.equal(fields[0].name, 'result'); + + connection.end((err) => { + assert.ifError(err); + process.exit(0); + }); + }); +}); diff --git a/test/esm/integration/connection/test-select-empty-string.test.mts b/test/esm/integration/connection/test-select-empty-string.test.mts new file mode 100644 index 0000000000..b09b8a6cbf --- /dev/null +++ b/test/esm/integration/connection/test-select-empty-string.test.mts @@ -0,0 +1,21 @@ +import type { FieldPacket, RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +let rows: RowDataPacket[], fields: FieldPacket[]; +connection.query('SELECT ""', (err, _rows, _fields) => { + if (err) { + throw err; + } + + rows = _rows; + fields = _fields; + connection.end(); +}); + +process.on('exit', () => { + assert.deepEqual(rows, [{ [fields[0].name]: '' }]); +}); diff --git a/test/esm/integration/connection/test-select-json.test.mts b/test/esm/integration/connection/test-select-json.test.mts new file mode 100644 index 0000000000..23cc3e306d --- /dev/null +++ b/test/esm/integration/connection/test-select-json.test.mts @@ -0,0 +1,39 @@ +/** + * Created by Alexander Panko on 2016.09.23 18:02 + * issue#409: https://github.com/sidorares/node-mysql2/issues/409 + */ +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +let textFetchedRows: RowDataPacket[] = []; +let binaryFetchedRows: RowDataPacket[] = []; + +const face = '\uD83D\uDE02'; + +connection.query('CREATE TEMPORARY TABLE json_test (json_test JSON)'); +connection.query('INSERT INTO json_test VALUES (?)', JSON.stringify(face)); +connection.query('SELECT * FROM json_test', (err, _rows) => { + if (err) { + throw err; + } + textFetchedRows = _rows; + connection.execute( + 'SELECT * FROM json_test', + (err, _rows) => { + if (err) { + throw err; + } + binaryFetchedRows = _rows; + connection.end(); + } + ); +}); + +process.on('exit', () => { + assert.equal(textFetchedRows[0].json_test, face); + assert.equal(binaryFetchedRows[0].json_test, face); +}); diff --git a/test/esm/integration/connection/test-select-negative.test.mts b/test/esm/integration/connection/test-select-negative.test.mts new file mode 100644 index 0000000000..1617f04260 --- /dev/null +++ b/test/esm/integration/connection/test-select-negative.test.mts @@ -0,0 +1,29 @@ +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +let rows: RowDataPacket[] = []; +let rows1: RowDataPacket[] = []; + +connection.execute('SELECT -1 v', [], (err, _rows) => { + if (err) { + throw err; + } + rows = _rows; +}); + +connection.query('SELECT -1 v', (err, _rows) => { + if (err) { + throw err; + } + rows1 = _rows; + connection.end(); +}); + +process.on('exit', () => { + assert.deepEqual(rows, [{ v: -1 }]); + assert.deepEqual(rows1, [{ v: -1 }]); +}); diff --git a/test/esm/integration/connection/test-select-ssl.test.mts b/test/esm/integration/connection/test-select-ssl.test.mts new file mode 100644 index 0000000000..79c305abfe --- /dev/null +++ b/test/esm/integration/connection/test-select-ssl.test.mts @@ -0,0 +1,40 @@ +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +type SslCipherRow = RowDataPacket & { + Variable_name: string; + Value: string; +}; + +const connection = createConnection(); + +connection.query( + `SHOW STATUS LIKE 'Ssl_cipher'`, + (err, rows) => { + assert.ifError(err); + if (process.env.MYSQL_USE_TLS === '1') { + assert.equal(rows[0].Value.length > 0, true); + } else { + assert.deepEqual(rows, [{ Variable_name: 'Ssl_cipher', Value: '' }]); + } + + connection.execute( + `SHOW STATUS LIKE 'Ssl_cipher'`, + (err, rows) => { + assert.ifError(err); + if (process.env.MYSQL_USE_TLS === '1') { + assert.equal(rows[0].Value.length > 0, true); + } else { + assert.deepEqual(rows, [{ Variable_name: 'Ssl_cipher', Value: '' }]); + } + + connection.end((err) => { + assert.ifError(err); + process.exit(0); + }); + } + ); + } +); diff --git a/test/esm/integration/connection/test-select-utf8.test.mts b/test/esm/integration/connection/test-select-utf8.test.mts new file mode 100644 index 0000000000..1f7079c514 --- /dev/null +++ b/test/esm/integration/connection/test-select-utf8.test.mts @@ -0,0 +1,23 @@ +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +let rows: RowDataPacket[] = []; +const multibyteText = '本日は晴天なり'; +connection.query( + `SELECT '${multibyteText}' as result`, + (err, _rows) => { + if (err) { + throw err; + } + rows = _rows; + connection.end(); + } +); + +process.on('exit', () => { + assert.equal(rows[0].result, multibyteText); +}); diff --git a/test/esm/integration/connection/test-server-listen.test.mts b/test/esm/integration/connection/test-server-listen.test.mts new file mode 100644 index 0000000000..77d5eeadfe --- /dev/null +++ b/test/esm/integration/connection/test-server-listen.test.mts @@ -0,0 +1,42 @@ +import type { Server } from '../../../../typings/mysql/lib/Server.js'; +import { assert } from 'poku'; +import mysql from '../../../../index.js'; + +// Verifies that the Server.listen can be called with any combination of +// pararameters valid for net.Server.listen. + +function testListen( + argsDescription: string, + listenCaller: (server: Server, callback: () => void) => void +) { + // @ts-expect-error: TODO: implement typings + const server = mysql.createServer(); + let listenCallbackFired = false; + + listenCaller(server, () => { + listenCallbackFired = true; + }); + setTimeout(() => { + assert.ok( + listenCallbackFired, + `Callback for call with ${argsDescription} did not fire` + ); + // @ts-expect-error: internal access + server._server.close(); + }, 100); +} + +testListen('port', (server, callback) => { + // @ts-expect-error: TODO: implement typings + server.listen(0, callback); +}); + +testListen('port, host', (server, callback) => { + // @ts-expect-error: TODO: implement typings + server.listen(0, '127.0.0.1', callback); +}); + +testListen('port, host, backlog', (server, callback) => { + // @ts-expect-error: TODO: implement typings + server.listen(0, '127.0.0.1', 50, callback); +}); diff --git a/test/esm/integration/connection/test-signed-tinyint.test.mts b/test/esm/integration/connection/test-signed-tinyint.test.mts new file mode 100644 index 0000000000..d15f3006cc --- /dev/null +++ b/test/esm/integration/connection/test-signed-tinyint.test.mts @@ -0,0 +1,33 @@ +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +let rows: RowDataPacket[] = []; + +connection.query( + 'CREATE TEMPORARY TABLE signed_ints (b11 tinyint NOT NULL, b12 tinyint NOT NULL, b21 smallint NOT NULL)' +); +connection.query('INSERT INTO signed_ints values (-3, -120, 500)'); +connection.query('INSERT INTO signed_ints values (3, -110, -500)'); + +connection.execute( + 'SELECT * from signed_ints', + [5], + (err, _rows) => { + if (err) { + throw err; + } + rows = _rows; + connection.end(); + } +); + +process.on('exit', () => { + assert.deepEqual(rows, [ + { b11: -3, b12: -120, b21: 500 }, + { b11: 3, b12: -110, b21: -500 }, + ]); +}); diff --git a/test/esm/integration/connection/test-stream-error-destroy-connection.test.mts b/test/esm/integration/connection/test-stream-error-destroy-connection.test.mts new file mode 100644 index 0000000000..452e8a630b --- /dev/null +++ b/test/esm/integration/connection/test-stream-error-destroy-connection.test.mts @@ -0,0 +1,100 @@ +import type { Connection } from '../../../../index.js'; +import { afterEach, assert, beforeEach, describe, it } from 'poku'; +import { config, createConnection } from '../../common.test.mjs'; + +const { database: currentDatabase } = config; + +describe('test stream error destroy connection:', async () => { + let connection: Connection; + + beforeEach(() => (connection = createConnection())); + + afterEach(async () => { + await connection.end(); + }); + + await it('Ensure stream ends in case of error', async () => { + connection.query( + [ + 'CREATE TEMPORARY TABLE `items` (', + '`id` int(11) NOT NULL AUTO_INCREMENT,', + '`text` varchar(255) DEFAULT NULL,', + 'PRIMARY KEY (`id`)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8', + ].join('\n'), + (err) => { + if (err) { + throw err; + } + } + ); + + for (let i = 0; i < 100; i++) { + connection.execute( + 'INSERT INTO items(text) VALUES(?)', + ['test'], + (err) => { + if (err) { + throw err; + } + } + ); + } + + const rows = connection.query('SELECT * FROM items').stream(); + + for await (const _ of rows) break; // forces return () -> destroy() + }); + + await it('end: Ensure stream emits error then close on server-side query error', async () => { + const state: { uncaughtExceptionError: Error | null } = { + uncaughtExceptionError: null, + }; + + const stream = connection + .query('SELECT invalid_column FROM invalid_table') + .stream(); + + stream.on('error', (error: Error) => { + state.uncaughtExceptionError = error; + }); + + await new Promise((resolve) => stream.on('end', resolve)); + + const uncaughtExceptionError = state.uncaughtExceptionError; + if (uncaughtExceptionError === null) { + assert.fail('Expected an uncaught exception error'); + } else { + assert.equal( + uncaughtExceptionError.message, + `Table '${currentDatabase}.invalid_table' doesn't exist` + ); + } + }); + + await it('close: Ensure stream emits error then close on server-side query error', async () => { + const state: { uncaughtExceptionError: Error | null } = { + uncaughtExceptionError: null, + }; + + const stream = connection + .query('SELECT invalid_column FROM invalid_table') + .stream(); + + stream.on('error', (error: Error) => { + state.uncaughtExceptionError = error; + }); + + await new Promise((resolve) => stream.on('close', resolve)); + + const uncaughtExceptionError = state.uncaughtExceptionError; + if (uncaughtExceptionError === null) { + assert.fail('Expected an uncaught exception error'); + } else { + assert.equal( + uncaughtExceptionError.message, + `Table '${currentDatabase}.invalid_table' doesn't exist` + ); + } + }); +}); diff --git a/test/esm/integration/connection/test-stream-errors.test.mts b/test/esm/integration/connection/test-stream-errors.test.mts new file mode 100644 index 0000000000..db4f25428a --- /dev/null +++ b/test/esm/integration/connection/test-stream-errors.test.mts @@ -0,0 +1,98 @@ +// This file was modified by Oracle on January 21, 2021. +// The connection with the mock server needs to happen in the same host where +// the tests are running in order to avoid connecting a potential MySQL server +// instance running in the host identified by the MYSQL_HOST environment +// variable. +// Modifications copyright (c) 2021, Oracle and/or its affiliates. + +import type { Connection } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection, createServer } from '../../common.test.mjs'; + +type TestError = Error & { code?: string; fatal?: boolean }; + +// The process is not terminated in Deno +if (typeof Deno !== 'undefined') process.exit(0); + +let clientConnection: Connection | undefined; +const err: Error & { code?: string } = new Error( + 'This socket has been ended by the other party' +); +err.code = 'EPIPE'; + +let receivedError1: TestError | undefined; +let receivedError2: TestError | undefined; +let receivedError3: TestError | undefined; + +const query = 'SELECT 1'; + +const server = createServer( + () => { + clientConnection = createConnection({ + // The mock server is running on the same host machine. + // We need to explicitly define the host to avoid connecting to a potential + // different host provided via MYSQL_HOST that identifies a real MySQL + // server instance. + host: 'localhost', + // @ts-expect-error: internal access + port: server._port, + // @ts-expect-error: TODO: implement typings + ssl: false, + }); + clientConnection?.query(query, (_err) => { + if (_err && _err.code === 'HANDSHAKE_NO_SSL_SUPPORT') { + clientConnection?.end(); + } + receivedError1 = _err ?? undefined; + }); + clientConnection?.query('second query, should not be executed', () => { + receivedError2 = err; + clientConnection?.query( + 'trying to enqueue command to a connection which is already in error state', + (_err1) => { + receivedError3 = _err1 ?? undefined; + } + ); + }); + }, + (conn) => { + conn.on('query', () => { + // @ts-expect-error: TODO: implement typings + conn.writeColumns([ + { + catalog: 'def', + schema: '', + table: '', + orgTable: '', + name: '1', + orgName: '', + characterSet: 63, + columnLength: 1, + columnType: 8, + flags: 129, + decimals: 0, + }, + ]); + // emulate stream error here + // @ts-expect-error: TODO: implement typings + clientConnection?.stream.emit('error', err); + // @ts-expect-error: TODO: implement typings + clientConnection?.stream.end(); + // @ts-expect-error: TODO: implement typings + server.close(); + }); + } +); + +process.on('exit', () => { + assert.equal(receivedError1?.fatal, true); + assert.equal(receivedError1?.code, err.code); + assert.equal(receivedError2?.fatal, true); + assert.equal(receivedError2?.code, err.code); + assert.equal(receivedError3?.fatal, true); + assert.equal( + receivedError3?.message, + "Can't add new command when connection is in closed state" + ); +}); diff --git a/test/esm/integration/connection/test-stream.test.mts b/test/esm/integration/connection/test-stream.test.mts new file mode 100644 index 0000000000..07928b6154 --- /dev/null +++ b/test/esm/integration/connection/test-stream.test.mts @@ -0,0 +1,83 @@ +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +let rows: RowDataPacket[]; +const rows1: RowDataPacket[] = []; +const rows2: RowDataPacket[] = []; +const rows3: RowDataPacket[] = []; +const rows4: RowDataPacket[] = []; + +connection.query( + [ + 'CREATE TEMPORARY TABLE `announcements` (', + '`id` int(11) NOT NULL AUTO_INCREMENT,', + '`title` varchar(255) DEFAULT NULL,', + '`text` varchar(255) DEFAULT NULL,', + 'PRIMARY KEY (`id`)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8', + ].join('\n'), + (err) => { + if (err) { + throw err; + } + } +); + +connection.execute( + 'INSERT INTO announcements(title, text) VALUES(?, ?)', + ['Есть место, где заканчивается тротуар', 'Расти борода, расти'], + (err) => { + if (err) { + throw err; + } + } +); +connection.execute( + 'INSERT INTO announcements(title, text) VALUES(?, ?)', + [ + 'Граждане Российской Федерации имеют право собираться мирно без оружия', + 'проводить собрания, митинги и демонстрации, шествия и пикетирование', + ], + (err) => { + if (err) { + throw err; + } + } +); +connection.execute('SELECT * FROM announcements', async (_err, _rows) => { + rows = _rows as RowDataPacket[]; + const s1 = connection.query('SELECT * FROM announcements').stream(); + s1.on('data', (row: RowDataPacket) => { + rows1.push(row); + }); + s1.on('end', () => { + const s2 = connection.execute('SELECT * FROM announcements').stream(); + s2.on('data', (row: RowDataPacket) => { + rows2.push(row); + }); + s2.on('end', () => { + connection.end(); + }); + }); + const s3 = connection.query('SELECT * FROM announcements').stream(); + for await (const row of s3) { + rows3.push(row as RowDataPacket); + } + const s4 = connection.query('SELECT * FROM announcements').stream(); + for await (const row of s4) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + rows4.push(row as RowDataPacket); + } +}); + +process.on('exit', () => { + assert.deepEqual(rows.length, 2); + assert.deepEqual(rows, rows1); + assert.deepEqual(rows, rows2); + assert.deepEqual(rows, rows3); + assert.deepEqual(rows, rows4); +}); diff --git a/test/esm/integration/connection/test-then-on-query.test.mts b/test/esm/integration/connection/test-then-on-query.test.mts new file mode 100644 index 0000000000..85ee5e864b --- /dev/null +++ b/test/esm/integration/connection/test-then-on-query.test.mts @@ -0,0 +1,22 @@ +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +let error = true; + +const q = connection.query('SELECT 1'); +try { + // @ts-expect-error: testing that .then does not exist on Query + if (q.then) q.then(); +} catch { + error = false; +} +q.on('end', () => { + connection.end(); +}); + +process.on('exit', () => { + assert.equal(error, false); +}); diff --git a/test/esm/integration/connection/test-timestamp.test.mts b/test/esm/integration/connection/test-timestamp.test.mts new file mode 100644 index 0000000000..d1d9433014 --- /dev/null +++ b/test/esm/integration/connection/test-timestamp.test.mts @@ -0,0 +1,62 @@ +import type { FieldPacket, RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +connection.query('SET SQL_MODE="ALLOW_INVALID_DATES";'); +connection.query('CREATE TEMPORARY TABLE t (f TIMESTAMP)'); +connection.query("INSERT INTO t VALUES('0000-00-00 00:00:00')"); +connection.query("INSERT INTO t VALUES('2013-01-22 01:02:03')"); + +let rows: RowDataPacket[], fields: FieldPacket[]; +let rows1: RowDataPacket[], fields1: FieldPacket[]; +let rows2: RowDataPacket[]; +connection.query('SELECT f FROM t', (err, _rows, _fields) => { + if (err) { + throw err; + } + rows = _rows; + fields = _fields; +}); +connection.execute( + 'SELECT f FROM t', + (err, _rows, _fields) => { + if (err) { + throw err; + } + rows1 = _rows; + fields1 = _fields; + } +); + +// test 11-byte timestamp - https://github.com/sidorares/node-mysql2/issues/254 +connection.execute( + 'SELECT CURRENT_TIMESTAMP(6) as t11', + (err, _rows) => { + if (err) { + throw err; + } + rows2 = _rows; + connection.end(); + } +); + +process.on('exit', () => { + assert.deepEqual(rows[0].f.toString(), 'Invalid Date'); + assert(rows[0].f instanceof Date); + assert(rows[1].f instanceof Date); + assert.equal(rows[1].f.getYear(), 113); + assert.equal(rows[1].f.getMonth(), 0); + assert.equal(rows[1].f.getDate(), 22); + assert.equal(rows[1].f.getHours(), 1); + assert.equal(rows[1].f.getMinutes(), 2); + assert.equal(rows[1].f.getSeconds(), 3); + assert.equal(fields[0].name, 'f'); + assert.deepEqual(rows[1], rows1[1]); + // @ts-expect-error: TODO: implement typings + assert.deepEqual(fields[0].inspect(), fields1[0].inspect()); + + assert(rows2[0].t11 instanceof Date); +}); diff --git a/test/esm/integration/connection/test-track-state-change.test.mts b/test/esm/integration/connection/test-track-state-change.test.mts new file mode 100644 index 0000000000..b58a61edf1 --- /dev/null +++ b/test/esm/integration/connection/test-track-state-change.test.mts @@ -0,0 +1,49 @@ +import type { ResultSetHeader } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +type CharsetStateChangeResult = ResultSetHeader & { + stateChanges: { + systemVariables: { + character_set_connection: string; + character_set_client: string; + character_set_results: string; + }; + }; +}; + +type SchemaStateChangeResult = ResultSetHeader & { + stateChanges: { + schema: string; + }; +}; + +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test for planetscale'); + process.exit(0); +} + +const connection = createConnection(); + +let result1: CharsetStateChangeResult, result2: SchemaStateChangeResult; + +connection.query('SET NAMES koi8r', (err, _ok) => { + assert.ifError(err); + result1 = _ok; +}); + +connection.query('USE mysql', (err, _ok) => { + assert.ifError(err); + result2 = _ok; + connection.end(); +}); + +process.on('exit', () => { + assert.deepEqual(result1.stateChanges.systemVariables, { + character_set_connection: 'koi8r', + character_set_client: 'koi8r', + character_set_results: 'koi8r', + }); + assert.deepEqual(result2.stateChanges.schema, 'mysql'); +}); diff --git a/test/esm/integration/connection/test-transaction-commit.test.mts b/test/esm/integration/connection/test-transaction-commit.test.mts new file mode 100644 index 0000000000..1d1499e2d6 --- /dev/null +++ b/test/esm/integration/connection/test-transaction-commit.test.mts @@ -0,0 +1,49 @@ +import type { RowDataPacket } from '../../../../index.js'; +import { assert } from 'poku'; +import { createConnection, useTestDb } from '../../common.test.mjs'; + +type TransactionRow = RowDataPacket & { + id: number; + title: string; +}; + +const connection = createConnection(); + +useTestDb(); + +const table = 'transaction_test'; +connection.query( + [ + `CREATE TEMPORARY TABLE \`${table}\` (`, + '`id` int(11) unsigned NOT NULL AUTO_INCREMENT,', + '`title` varchar(255),', + 'PRIMARY KEY (`id`)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8', + ].join('\n') +); + +connection.beginTransaction((err) => { + assert.ifError(err); + + const row = { + id: 1, + title: 'Test row', + }; + + connection.query(`INSERT INTO ${table} SET ?`, row, (err) => { + assert.ifError(err); + + connection.commit((err) => { + assert.ifError(err); + + connection.query( + `SELECT * FROM ${table}`, + (err, rows) => { + assert.ifError(err); + connection.end(); + assert.equal(rows?.length, 1); + } + ); + }); + }); +}); diff --git a/test/esm/integration/connection/test-transaction-rollback.test.mts b/test/esm/integration/connection/test-transaction-rollback.test.mts new file mode 100644 index 0000000000..eee5696122 --- /dev/null +++ b/test/esm/integration/connection/test-transaction-rollback.test.mts @@ -0,0 +1,44 @@ +import type { RowDataPacket } from '../../../../index.js'; +import { assert } from 'poku'; +import { createConnection, useTestDb } from '../../common.test.mjs'; + +const connection = createConnection(); + +useTestDb(); + +const table = 'transaction_test'; +connection.query( + [ + `CREATE TEMPORARY TABLE \`${table}\` (`, + '`id` int(11) unsigned NOT NULL AUTO_INCREMENT,', + '`title` varchar(255),', + 'PRIMARY KEY (`id`)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8', + ].join('\n') +); + +connection.beginTransaction((err) => { + assert.ifError(err); + + const row = { + id: 1, + title: 'Test row', + }; + + connection.query(`INSERT INTO ${table} SET ?`, row, (err) => { + assert.ifError(err); + + connection.rollback((err) => { + assert.ifError(err); + + connection.query( + `SELECT * FROM ${table}`, + (err, rows) => { + assert.ifError(err); + connection.end(); + assert.equal(rows.length, 0); + } + ); + }); + }); +}); diff --git a/test/esm/integration/connection/test-type-cast-null-fields-execute.test.mts b/test/esm/integration/connection/test-type-cast-null-fields-execute.test.mts new file mode 100644 index 0000000000..743f81f098 --- /dev/null +++ b/test/esm/integration/connection/test-type-cast-null-fields-execute.test.mts @@ -0,0 +1,55 @@ +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection, useTestDb } from '../../common.test.mjs'; + +type InsertTestRow = RowDataPacket & { + id: number; + date: string | null; + number: number | null; +}; + +const connection = createConnection(); + +useTestDb(); + +const table = 'insert_test'; +connection.execute( + [ + `CREATE TEMPORARY TABLE \`${table}\` (`, + '`id` int(11) unsigned NOT NULL AUTO_INCREMENT,', + '`date` DATETIME NULL,', + '`number` INT NULL,', + 'PRIMARY KEY (`id`)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8', + ].join('\n'), + (err) => { + if (err) throw err; + } +); + +connection.execute( + `INSERT INTO ${table} (date, number) VALUES (?, ?)`, + [null, null], + (err) => { + if (err) throw err; + } +); + +let results: InsertTestRow[]; +connection.execute( + `SELECT * FROM ${table}`, + (err, _results) => { + if (err) { + throw err; + } + + results = _results; + connection.end(); + } +); + +process.on('exit', () => { + assert.strictEqual(results[0].date, null); + assert.strictEqual(results[0].number, null); +}); diff --git a/test/esm/integration/connection/test-type-cast-null-fields.test.mts b/test/esm/integration/connection/test-type-cast-null-fields.test.mts new file mode 100644 index 0000000000..4db07e5177 --- /dev/null +++ b/test/esm/integration/connection/test-type-cast-null-fields.test.mts @@ -0,0 +1,49 @@ +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection, useTestDb } from '../../common.test.mjs'; + +type InsertTestRow = RowDataPacket & { + id: number; + date: string | null; + number: number | null; +}; + +const connection = createConnection(); + +useTestDb(); + +const table = 'insert_test'; +connection.query( + [ + `CREATE TEMPORARY TABLE \`${table}\` (`, + '`id` int(11) unsigned NOT NULL AUTO_INCREMENT,', + '`date` DATETIME NULL,', + '`number` INT NULL,', + 'PRIMARY KEY (`id`)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8', + ].join('\n') +); + +connection.query(`INSERT INTO ${table} SET ?`, { + date: null, + number: null, +}); + +let results: InsertTestRow[]; +connection.query( + `SELECT * FROM ${table}`, + (_err, _results) => { + if (_err) { + throw _err; + } + + results = _results; + connection.end(); + } +); + +process.on('exit', () => { + assert.strictEqual(results[0].date, null); + assert.strictEqual(results[0].number, null); +}); diff --git a/test/esm/integration/connection/test-type-casting-execute.test.mts b/test/esm/integration/connection/test-type-casting-execute.test.mts new file mode 100644 index 0000000000..55a8f54758 --- /dev/null +++ b/test/esm/integration/connection/test-type-casting-execute.test.mts @@ -0,0 +1,134 @@ +import type { FieldPacket, RowDataPacket } from '../../../../index.js'; +import { Buffer } from 'node:buffer'; +import process from 'node:process'; +import { assert, test } from 'poku'; +import driver from '../../../../index.js'; +import { createConnection, useTestDb } from '../../common.test.mjs'; +import typeCastingTests from './type-casting-tests.test.mjs'; + +type TypeCastTest = { + type: string; + insert: string | number | Date | Buffer | null; + columnType: string; + expect?: unknown; + insertRaw?: string; + deep?: boolean; + columnName?: string; +}; + +const getTypeNameByCode = ( + columnType: number | undefined +): string | undefined => { + if (columnType === undefined) { + return undefined; + } + + for (const [key, value] of Object.entries(driver.Types)) { + const numericKey = Number(key); + if ( + !Number.isNaN(numericKey) && + numericKey === columnType && + typeof value === 'string' + ) { + return value; + } + } + + return undefined; +}; + +test(async () => { + const connection = createConnection(); + + useTestDb(); + + connection.execute('select 1', async (waitConnectErr) => { + assert.ifError(waitConnectErr); + + const tests = (await typeCastingTests(connection)) as TypeCastTest[]; + + const table = 'type_casting'; + + const schema: string[] = []; + const inserts: string[] = []; + + tests.forEach((test, index) => { + const escaped = test.insertRaw || connection.escape(test.insert); + + test.columnName = `${test.type}_${index}`; + + schema.push(`\`${test.columnName}\` ${test.type},`); + inserts.push(`\`${test.columnName}\` = ${escaped}`); + }); + + const createTable = [ + `CREATE TEMPORARY TABLE \`${table}\` (`, + '`id` int(11) unsigned NOT NULL AUTO_INCREMENT,', + ] + .concat(schema) + .concat(['PRIMARY KEY (`id`)', ') ENGINE=InnoDB DEFAULT CHARSET=utf8']) + .join('\n'); + + connection.execute(createTable); + + connection.execute(`INSERT INTO ${table} SET ${inserts.join(',\n')}`); + + let row: RowDataPacket | undefined; + let fieldData: Record = {}; + connection.execute( + `SELECT * FROM ${table}`, + (err, rows, fields) => { + if (err) { + throw err; + } + + row = rows[0]; + // build a fieldName: fieldType lookup table + fieldData = (fields as FieldPacket[]).reduce( + (a: Record, v) => { + a[v['name']] = v['type']; + return a; + }, + {} + ); + connection.end(); + } + ); + + process.on('exit', () => { + tests.forEach((test) => { + // check that the column type matches the type name stored in driver.Types + const columnType = fieldData[test.columnName ?? '']; + const columnTypeName = getTypeNameByCode(columnType); + assert.equal(test.columnType === columnTypeName, true, test.columnName); + let expected: unknown = test.expect || test.insert; + let got: unknown = row?.[test.columnName ?? '']; + let message: string; + + if (expected instanceof Date) { + assert.equal(got instanceof Date, true, test.type); + + expected = String(expected); + got = String(got); + } else if (Buffer.isBuffer(expected)) { + assert.equal(Buffer.isBuffer(got), true, test.type); + + expected = String(Array.prototype.slice.call(expected)); + got = String(Array.prototype.slice.call(got)); + } + + if (test.deep) { + message = `got: "${JSON.stringify(got)}" expected: "${JSON.stringify( + expected + )}" test: ${test.type}`; + assert.deepEqual(expected, got, message); + } else { + message = `got: "${got}" (${typeof got}) expected: "${expected}" (${typeof expected}) test: ${ + test.type + }`; + assert.strictEqual(expected, got, message); + } + }); + }); + }); +}); diff --git a/test/esm/integration/connection/test-type-casting.test.mts b/test/esm/integration/connection/test-type-casting.test.mts new file mode 100644 index 0000000000..159dd964de --- /dev/null +++ b/test/esm/integration/connection/test-type-casting.test.mts @@ -0,0 +1,134 @@ +import type { FieldPacket, RowDataPacket } from '../../../../index.js'; +import { Buffer } from 'node:buffer'; +import process from 'node:process'; +import { assert, test } from 'poku'; +import driver from '../../../../index.js'; +import { createConnection, useTestDb } from '../../common.test.mjs'; +import typeCastingTests from './type-casting-tests.test.mjs'; + +type TypeCastTest = { + type: string; + insert?: string | number | Date | Buffer | null; + insertRaw?: string; + expect?: unknown; + deep?: boolean; + columnType: string; + columnName?: string; +}; + +const getTypeNameByCode = ( + columnType: number | undefined +): string | undefined => { + if (columnType === undefined) { + return undefined; + } + + for (const [key, value] of Object.entries(driver.Types)) { + const numericKey = Number(key); + if ( + !Number.isNaN(numericKey) && + numericKey === columnType && + typeof value === 'string' + ) { + return value; + } + } + + return undefined; +}; + +test(async () => { + const connection = createConnection(); + + useTestDb(); + + connection.query('select 1', async (waitConnectErr) => { + assert.ifError(waitConnectErr); + + const tests: TypeCastTest[] = await typeCastingTests(connection); + + const table = 'type_casting'; + + const schema: string[] = []; + const inserts: string[] = []; + + tests.forEach((test, index) => { + const escaped = test.insertRaw || connection.escape(test.insert); + + test.columnName = `${test.type}_${index}`; + + schema.push(`\`${test.columnName}\` ${test.type},`); + inserts.push(`\`${test.columnName}\` = ${escaped}`); + }); + + const createTable = [ + `CREATE TEMPORARY TABLE \`${table}\` (`, + '`id` int(11) unsigned NOT NULL AUTO_INCREMENT,', + ] + .concat(schema) + .concat(['PRIMARY KEY (`id`)', ') ENGINE=InnoDB DEFAULT CHARSET=utf8']) + .join('\n'); + + connection.query(createTable); + + connection.query(`INSERT INTO ${table} SET${inserts.join(',\n')}`); + + let row: RowDataPacket | undefined; + let fieldData: Record = {}; + connection.query( + `SELECT * FROM ${table}`, + (err, rows, fields: FieldPacket[]) => { + if (err) { + throw err; + } + + row = rows[0]; + // build a fieldName: fieldType lookup table + fieldData = fields.reduce>( + (a, v) => { + a[v['name']] = v['type']; + return a; + }, + {} + ); + connection.end(); + } + ); + + process.on('exit', () => { + tests.forEach((test) => { + // check that the column type matches the type name stored in driver.Types + const columnType = fieldData[test.columnName ?? '']; + const columnTypeName = getTypeNameByCode(columnType); + assert.equal(test.columnType === columnTypeName, true, test.columnName); + let expected: unknown = test.expect || test.insert; + let got: unknown = row?.[test.columnName ?? '']; + let _message; + + if (expected instanceof Date) { + assert.equal(got instanceof Date, true, test.type); + + expected = String(expected); + got = String(got); + } else if (Buffer.isBuffer(expected)) { + assert.equal(Buffer.isBuffer(got), true, test.type); + + expected = String(Array.prototype.slice.call(expected)); + got = String(Array.prototype.slice.call(got)); + } + + if (test.deep) { + _message = `got: "${JSON.stringify(got)}" expected: "${JSON.stringify( + expected + )}" test: ${test.type}`; + assert.deepEqual(expected, got, _message); + } else { + _message = `got: "${got}" (${typeof got}) expected: "${expected}" (${typeof expected}) test: ${ + test.type + }`; + assert.strictEqual(expected, got, _message); + } + }); + }); + }); +}); diff --git a/test/esm/integration/connection/test-typecast-execute.test.mts b/test/esm/integration/connection/test-typecast-execute.test.mts new file mode 100644 index 0000000000..111b30676e --- /dev/null +++ b/test/esm/integration/connection/test-typecast-execute.test.mts @@ -0,0 +1,157 @@ +import type { + RowDataPacket, + TypeCastField, + TypeCastNext, +} from '../../../../index.js'; +import { Buffer } from 'node:buffer'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +type FooRow = RowDataPacket & { foo: string }; +type FooBufferRow = RowDataPacket & { foo: Buffer }; +type TestValueRow = RowDataPacket & { test: null; value: number }; +type JsonTestRow = RowDataPacket & { json_test: { test: number } }; +type GeomTestRow = RowDataPacket & { + p: { x: number; y: number }; + g: { x: number; y: number }[]; +}; + +const connection = createConnection(); + +connection.execute('CREATE TEMPORARY TABLE json_test (json_test JSON)'); +connection.execute('INSERT INTO json_test VALUES (?)', [ + JSON.stringify({ test: 42 }), +]); + +connection.execute( + 'CREATE TEMPORARY TABLE geom_test (p POINT, g GEOMETRY NOT NULL)' +); +connection.execute( + 'INSERT INTO geom_test VALUES (ST_GeomFromText(?), ST_GeomFromText(?))', + [ + 'POINT(1 1)', + 'LINESTRING(-71.160281 42.258729,-71.160837 42.259113,-71.161144 42.25932)', + ] +); + +connection.execute( + { + sql: 'select "foo uppercase" as foo', + typeCast: function (field: TypeCastField, next: TypeCastNext) { + assert.equal('number', typeof field.length); + if (field.type === 'VAR_STRING') { + return field.string()?.toUpperCase(); + } + return next(); + }, + }, + (err, res) => { + assert.ifError(err); + assert.equal(res[0].foo, 'FOO UPPERCASE'); + } +); + +connection.execute( + { + sql: 'select "foobar" as foo', + typeCast: false, + }, + (err, res) => { + assert.ifError(err); + assert(Buffer.isBuffer(res[0].foo)); + assert.equal(res[0].foo.toString('utf8'), 'foobar'); + } +); + +connection.execute( + { + sql: 'SELECT NULL as test, 6 as value;', + typeCast: function (_field: TypeCastField, next: TypeCastNext) { + return next(); + }, + }, + (err, _rows) => { + assert.ifError(err); + assert.equal(_rows[0].test, null); + assert.equal(_rows[0].value, 6); + } +); + +connection.execute( + { + sql: 'SELECT * from json_test', + typeCast: function (_field: TypeCastField, next: TypeCastNext) { + return next(); + }, + }, + (err, _rows) => { + assert.ifError(err); + assert.equal(_rows[0].json_test.test, 42); + } +); + +// read geo fields +connection.execute( + { + sql: 'select * from geom_test', + }, + (err, res) => { + assert.ifError(err); + assert.deepEqual({ x: 1, y: 1 }, res[0].p); + assert.deepEqual( + [ + { x: -71.160281, y: 42.258729 }, + { x: -71.160837, y: 42.259113 }, + { x: -71.161144, y: 42.25932 }, + ], + res[0].g + ); + } +); + +connection.execute( + { + sql: 'select * from geom_test', + typeCast: function (field: TypeCastField, _next: TypeCastNext) { + assert.equal('geom_test', field.table); + + if (field.name === 'p' && field.type === 'GEOMETRY') { + assert.deepEqual({ x: 1, y: 1 }, field.geometry()); + return { x: 2, y: 2 }; + } + + if (field.name === 'g' && field.type === 'GEOMETRY') { + assert.deepEqual( + [ + { x: -71.160281, y: 42.258729 }, + { x: -71.160837, y: 42.259113 }, + { x: -71.161144, y: 42.25932 }, + ], + field.geometry() + ); + + return [ + { x: -70, y: 40 }, + { x: -60, y: 50 }, + { x: -50, y: 60 }, + ]; + } + + assert.fail('should not reach here'); + }, + }, + (err, res) => { + assert.ifError(err); + assert.deepEqual({ x: 2, y: 2 }, res[0].p); + assert.deepEqual( + [ + { x: -70, y: 40 }, + { x: -60, y: 50 }, + { x: -50, y: 60 }, + ], + res[0].g + ); + } +); + +connection.end(); diff --git a/test/esm/integration/connection/test-typecast-geometry-execute.test.mts b/test/esm/integration/connection/test-typecast-geometry-execute.test.mts new file mode 100644 index 0000000000..7eb14f9d5f --- /dev/null +++ b/test/esm/integration/connection/test-typecast-geometry-execute.test.mts @@ -0,0 +1,52 @@ +import type { RowDataPacket, TypeCastGeometry } from '../../../../index.js'; +import { Buffer } from 'node:buffer'; +import { assert, test } from 'poku'; +import { createConnection, getMysqlVersion } from '../../common.test.mjs'; + +type GeometryRow = RowDataPacket & { foo: TypeCastGeometry }; +type BufferRow = RowDataPacket & { foo: Buffer }; + +test(async () => { + const connection = createConnection(); + const mySQLVersion = await getMysqlVersion(connection); + + connection.execute('select 1', () => { + // mysql8 renamed some standard functions + // see https://dev.mysql.com/doc/refman/8.0/en/gis-wkb-functions.html + const stPrefix = mySQLVersion.major >= 8 ? 'ST_' : ''; + + connection.execute( + { + sql: `select ${stPrefix}GeomFromText('POINT(11 0)') as foo`, + typeCast: function (field, next) { + if (field.type === 'GEOMETRY') { + return field.geometry(); + } + return next(); + }, + }, + (err, res) => { + assert.ifError(err); + assert.deepEqual(res[0].foo, { x: 11, y: 0 }); + } + ); + + connection.execute( + { + sql: `select ${stPrefix}GeomFromText('POINT(11 0)') as foo`, + typeCast: function (field, next) { + if (field.type === 'GEOMETRY') { + return field.buffer(); + } + return next(); + }, + }, + (err, res) => { + assert.ifError(err); + assert.equal(Buffer.isBuffer(res[0].foo), true); + } + ); + + connection.end(); + }); +}); diff --git a/test/esm/integration/connection/test-typecast-geometry.test.mts b/test/esm/integration/connection/test-typecast-geometry.test.mts new file mode 100644 index 0000000000..65727bc063 --- /dev/null +++ b/test/esm/integration/connection/test-typecast-geometry.test.mts @@ -0,0 +1,52 @@ +import type { RowDataPacket, TypeCastGeometry } from '../../../../index.js'; +import { Buffer } from 'node:buffer'; +import { assert, test } from 'poku'; +import { createConnection, getMysqlVersion } from '../../common.test.mjs'; + +type GeometryRow = RowDataPacket & { foo: TypeCastGeometry }; +type BufferRow = RowDataPacket & { foo: Buffer }; + +test(async () => { + const connection = createConnection(); + const mySQLVersion = await getMysqlVersion(connection); + + connection.query('select 1', () => { + // mysql8 renamed some standard functions + // see https://dev.mysql.com/doc/refman/8.0/en/gis-wkb-functions.html + const stPrefix = mySQLVersion.major >= 8 ? 'ST_' : ''; + + connection.query( + { + sql: `select ${stPrefix}GeomFromText('POINT(11 0)') as foo`, + typeCast: function (field, next) { + if (field.type === 'GEOMETRY') { + return field.geometry(); + } + return next(); + }, + }, + (err, res) => { + assert.ifError(err); + assert.deepEqual(res[0].foo, { x: 11, y: 0 }); + } + ); + + connection.query( + { + sql: `select ${stPrefix}GeomFromText('POINT(11 0)') as foo`, + typeCast: function (field, next) { + if (field.type === 'GEOMETRY') { + return field.buffer(); + } + return next(); + }, + }, + (err, res) => { + assert.ifError(err); + assert.equal(Buffer.isBuffer(res[0].foo), true); + } + ); + + connection.end(); + }); +}); diff --git a/test/esm/integration/connection/test-typecast-overwriting-execute.test.mts b/test/esm/integration/connection/test-typecast-overwriting-execute.test.mts new file mode 100644 index 0000000000..ec4e5595e9 --- /dev/null +++ b/test/esm/integration/connection/test-typecast-overwriting-execute.test.mts @@ -0,0 +1,52 @@ +import type { RowDataPacket } from '../../../../index.js'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +type TypecastRow = RowDataPacket & { foo: string }; + +const connection = createConnection({ + typeCast: function (field, next) { + assert.equal('number', typeof field.length); + if (field.type === 'VAR_STRING') { + const value = field.string(); + if (value === null) { + return value; + } + return value.toUpperCase(); + } + return next(); + }, +}); + +connection.execute( + { + sql: 'select "foo uppercase" as foo', + }, + (err, res) => { + assert.ifError(err); + assert.equal(res[0].foo, 'FOO UPPERCASE'); + } +); + +connection.execute( + { + sql: 'select "foo lowercase" as foo', + typeCast: function (field, next) { + assert.equal('number', typeof field.length); + if (field.type === 'VAR_STRING') { + const value = field.string(); + if (value === null) { + return value; + } + return value.toLowerCase(); + } + return next(); + }, + }, + (err, res) => { + assert.ifError(err); + assert.equal(res[0].foo, 'foo lowercase'); + } +); + +connection.end(); diff --git a/test/esm/integration/connection/test-typecast-overwriting.test.mts b/test/esm/integration/connection/test-typecast-overwriting.test.mts new file mode 100644 index 0000000000..aef4e3ec8d --- /dev/null +++ b/test/esm/integration/connection/test-typecast-overwriting.test.mts @@ -0,0 +1,52 @@ +import type { RowDataPacket } from '../../../../index.js'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +type TypecastRow = RowDataPacket & { foo: string }; + +const connection = createConnection({ + typeCast: function (field, next) { + assert.equal('number', typeof field.length); + if (field.type === 'VAR_STRING') { + const value = field.string(); + if (value === null) { + return value; + } + return value.toUpperCase(); + } + return next(); + }, +}); + +connection.query( + { + sql: 'select "foo uppercase" as foo', + }, + (err, res) => { + assert.ifError(err); + assert.equal(res[0].foo, 'FOO UPPERCASE'); + } +); + +connection.query( + { + sql: 'select "foo lowercase" as foo', + typeCast: function (field, next) { + assert.equal('number', typeof field.length); + if (field.type === 'VAR_STRING') { + const value = field.string(); + if (value === null) { + return value; + } + return value.toLowerCase(); + } + return next(); + }, + }, + (err, res) => { + assert.ifError(err); + assert.equal(res[0].foo, 'foo lowercase'); + } +); + +connection.end(); diff --git a/test/esm/integration/connection/test-typecast.test.mts b/test/esm/integration/connection/test-typecast.test.mts new file mode 100644 index 0000000000..fd5ef4cf6c --- /dev/null +++ b/test/esm/integration/connection/test-typecast.test.mts @@ -0,0 +1,159 @@ +import type { + RowDataPacket, + TypeCastField, + TypeCastNext, +} from '../../../../index.js'; +import { Buffer } from 'node:buffer'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +connection.query('CREATE TEMPORARY TABLE json_test (json_test JSON)'); +connection.query( + 'INSERT INTO json_test VALUES (?)', + JSON.stringify({ test: 42 }) +); + +connection.query( + 'CREATE TEMPORARY TABLE geom_test (p POINT, g GEOMETRY NOT NULL)' +); +connection.query( + 'INSERT INTO geom_test VALUES (ST_GeomFromText("POINT(1 1)"), ' + + 'ST_GeomFromText("LINESTRING(-71.160281 42.258729,-71.160837 42.259113,-71.161144 42.25932)"))' +); + +connection.query( + { + sql: 'select "foo uppercase" as foo', + typeCast: function (field: TypeCastField, next: TypeCastNext) { + assert.equal('number', typeof field.length); + if (field.type === 'VAR_STRING') { + return field.string()?.toUpperCase(); + } + return next(); + }, + }, + (err, res) => { + assert.ifError(err); + assert.equal(res[0].foo, 'FOO UPPERCASE'); + } +); + +connection.query( + { + sql: 'select "foobar" as foo', + typeCast: false, + }, + (err, res) => { + assert.ifError(err); + assert(Buffer.isBuffer(res[0].foo), 'Check for Buffer'); + assert.equal(res[0].foo.toString('utf8'), 'foobar'); + } +); + +connection.query( + { + sql: 'SELECT NULL as test, 6 as value;', + typeCast: function (_field: TypeCastField, next: TypeCastNext) { + return next(); + }, + }, + (err, _rows) => { + assert.ifError(err); + assert.equal(_rows[0].test, null); + assert.equal(_rows[0].value, 6); + } +); + +connection.query( + { + sql: 'SELECT * from json_test', + typeCast: function (_field: TypeCastField, next: TypeCastNext) { + return next(); + }, + }, + (err, _rows) => { + assert.ifError(err); + assert.equal(_rows[0].json_test.test, 42); + } +); + +connection.execute( + { + sql: 'SELECT * from json_test', + typeCast: function (_field: TypeCastField, next: TypeCastNext) { + return next(); + }, + }, + (err, _rows) => { + assert.ifError(err); + assert.equal(_rows[0].json_test.test, 42); + } +); + +// read geo fields +connection.query( + { + sql: 'select * from geom_test', + }, + (err, res) => { + assert.ifError(err); + assert.deepEqual({ x: 1, y: 1 }, res[0].p); + assert.deepEqual( + [ + { x: -71.160281, y: 42.258729 }, + { x: -71.160837, y: 42.259113 }, + { x: -71.161144, y: 42.25932 }, + ], + res[0].g + ); + } +); + +connection.query( + { + sql: 'select * from geom_test', + typeCast: function (field: TypeCastField, _next: TypeCastNext) { + assert.equal('geom_test', field.table); + + if (field.name === 'p' && field.type === 'GEOMETRY') { + assert.deepEqual({ x: 1, y: 1 }, field.geometry()); + return { x: 2, y: 2 }; + } + + if (field.name === 'g' && field.type === 'GEOMETRY') { + assert.deepEqual( + [ + { x: -71.160281, y: 42.258729 }, + { x: -71.160837, y: 42.259113 }, + { x: -71.161144, y: 42.25932 }, + ], + field.geometry() + ); + + return [ + { x: -70, y: 40 }, + { x: -60, y: 50 }, + { x: -50, y: 60 }, + ]; + } + + assert.fail('should not reach here'); + }, + }, + (err, res) => { + assert.ifError(err); + assert.deepEqual({ x: 2, y: 2 }, res[0].p); + assert.deepEqual( + [ + { x: -70, y: 40 }, + { x: -60, y: 50 }, + { x: -50, y: 60 }, + ], + res[0].g + ); + } +); + +connection.end(); diff --git a/test/esm/integration/connection/test-update-changed-rows.test.mts b/test/esm/integration/connection/test-update-changed-rows.test.mts new file mode 100644 index 0000000000..b3d826c3c5 --- /dev/null +++ b/test/esm/integration/connection/test-update-changed-rows.test.mts @@ -0,0 +1,66 @@ +import type { ResultSetHeader } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +// "changedRows" is not part of the mysql protocol and extracted from "info string" response +// while valid for most mysql servers, it's not guaranteed to be present in all cases +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test for planetscale'); + process.exit(0); +} + +/** + * created at 2016.09.17 15:24:34 + * + * issue#288: https://github.com/sidorares/node-mysql2/issues/288 + */ +const connection = createConnection(); + +let result1: ResultSetHeader; +let result2: ResultSetHeader; + +connection.query( + [ + 'CREATE TEMPORARY TABLE `changed_rows` (', + '`id` int(11) unsigned NOT NULL AUTO_INCREMENT,', + '`value` int(5) NOT NULL,', + 'PRIMARY KEY (`id`)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8', + ].join('\n') +); +connection.query('insert into changed_rows(value) values(1)'); +connection.query('insert into changed_rows(value) values(1)'); +connection.query('insert into changed_rows(value) values(2)'); +connection.query('insert into changed_rows(value) values(3)'); + +connection.execute( + 'update changed_rows set value=1', + [], + (err, _result) => { + if (err) { + throw err; + } + + result1 = _result; + connection.execute( + 'update changed_rows set value=1', + [], + (err, _result) => { + if (err) { + throw err; + } + + result2 = _result; + connection.end(); + } + ); + } +); + +process.on('exit', () => { + assert.equal(result1.affectedRows, 4); + assert.equal(result1.changedRows, 2); + assert.equal(result2.affectedRows, 4); + assert.equal(result2.changedRows, 0); +}); diff --git a/test/esm/integration/connection/test-vector.test.mts b/test/esm/integration/connection/test-vector.test.mts index 94f6a00852..64872b752f 100644 --- a/test/esm/integration/connection/test-vector.test.mts +++ b/test/esm/integration/connection/test-vector.test.mts @@ -1,6 +1,9 @@ -import { it, assert, describe } from 'poku'; +import type { RowDataPacket } from '../../../../index.js'; +import { assert, describe, it } from 'poku'; import { createConnection, getMysqlVersion } from '../../common.test.mjs'; +type VectorRow = RowDataPacket & { test: number[] }; + const sql = `SELECT TO_VECTOR("[1.05, -17.8, 32, 123.456]") as test`; const expectedArray: number[] = [1.05, -17.8, 32, 123.456]; const epsilon: number = 1e-6; @@ -23,7 +26,7 @@ await describe(async () => { } await it('Execute PS with vector response is parsed correctly', async () => { - const [_rows] = await connection.execute(sql); + const [_rows] = await connection.execute(sql); assert.equal( compareFLoatsArray(_rows[0].test, expectedArray), true, @@ -32,7 +35,7 @@ await describe(async () => { }); await it('Select returning vector is parsed correctly', async () => { - const [_rows] = await connection.query(sql); + const [_rows] = await connection.query(sql); assert.equal( compareFLoatsArray(_rows[0].test, expectedArray), true, diff --git a/test/esm/integration/connection/type-casting-tests.test.mts b/test/esm/integration/connection/type-casting-tests.test.mts new file mode 100644 index 0000000000..fd6ad0a628 --- /dev/null +++ b/test/esm/integration/connection/type-casting-tests.test.mts @@ -0,0 +1,221 @@ +import type { Connection } from '../../../../index.js'; +import { Buffer } from 'node:buffer'; +import { getMysqlVersion } from '../../common.test.mjs'; + +export default async function (connection: Connection) { + const mySQLVersion = await getMysqlVersion(connection); + + // mysql8 renamed some standard functions + // see https://dev.mysql.com/doc/refman/8.0/en/gis-wkb-functions.html + const stPrefix = mySQLVersion.major >= 8 ? 'ST_' : ''; + + return [ + { type: 'decimal(4,3)', insert: '1.234', columnType: 'NEWDECIMAL' }, + // {type: 'decimal(3,3)', insert: 0.33}, + { type: 'tinyint', insert: 1, columnType: 'TINY' }, + { type: 'smallint', insert: 2, columnType: 'SHORT' }, + { type: 'int', insert: 3, columnType: 'LONG' }, + { type: 'float', insert: 4.5, columnType: 'FLOAT' }, + { type: 'double', insert: 5.5, columnType: 'DOUBLE' }, + { type: 'bigint', insert: '6', expect: 6, columnType: 'LONGLONG' }, + { type: 'bigint', insert: 6, columnType: 'LONGLONG' }, + { type: 'mediumint', insert: 7, columnType: 'INT24' }, + { type: 'year', insert: 2012, columnType: 'YEAR' }, + { + type: 'timestamp', + insert: new Date('2012-05-12 11:00:23'), + columnType: 'TIMESTAMP', + }, + { + type: 'datetime', + insert: new Date('2012-05-12 12:00:23'), + columnType: 'DATETIME', + }, + { + type: 'date', + insert: new Date('2012-05-12 00:00:00'), + columnType: 'DATE', + }, + { type: 'time', insert: '13:13:23', columnType: 'TIME' }, + { type: 'time', insert: '-13:13:23', columnType: 'TIME' }, + { type: 'time', insert: '413:13:23', columnType: 'TIME' }, + { type: 'time', insert: '-413:13:23', columnType: 'TIME' }, + { + type: 'binary(4)', + insert: Buffer.from([0, 1, 254, 255]), + columnType: 'STRING', + }, + { + type: 'varbinary(4)', + insert: Buffer.from([0, 1, 254, 255]), + columnType: 'VAR_STRING', + }, + { + type: 'tinyblob', + insert: Buffer.from([0, 1, 254, 255]), + columnType: 'BLOB', + }, + { + type: 'mediumblob', + insert: Buffer.from([0, 1, 254, 255]), + columnType: 'BLOB', + }, + { + type: 'longblob', + insert: Buffer.from([0, 1, 254, 255]), + columnType: 'BLOB', + }, + { type: 'blob', insert: Buffer.from([0, 1, 254, 255]), columnType: 'BLOB' }, + { + type: 'bit(32)', + insert: Buffer.from([0, 1, 254, 255]), + columnType: 'BIT', + }, + { type: 'char(5)', insert: 'Hello', columnType: 'STRING' }, + { type: 'varchar(5)', insert: 'Hello', columnType: 'VAR_STRING' }, + { + type: 'varchar(3) character set utf8 collate utf8_bin', + insert: 'bin', + columnType: 'VAR_STRING', + }, + { type: 'tinytext', insert: 'Hello World', columnType: 'BLOB' }, + { type: 'mediumtext', insert: 'Hello World', columnType: 'BLOB' }, + { type: 'longtext', insert: 'Hello World', columnType: 'BLOB' }, + { type: 'text', insert: 'Hello World', columnType: 'BLOB' }, + { + type: 'point', + insertRaw: 'POINT(1.2,-3.4)', + expect: { x: 1.2, y: -3.4 }, + deep: true, + columnType: 'GEOMETRY', + }, + { + type: 'point', + insertRaw: (function () { + const buffer = Buffer.alloc(21); + buffer.writeUInt8(1, 0); + buffer.writeUInt32LE(1, 1); + buffer.writeDoubleLE(-5.6, 5); + buffer.writeDoubleLE(10.23, 13); + return `${stPrefix}GeomFromWKB(${connection.escape(buffer)})`; + })(), + expect: { x: -5.6, y: 10.23 }, + deep: true, + columnType: 'GEOMETRY', + }, + { + type: 'point', + insertRaw: '', + insert: null, + expect: null, + columnType: 'GEOMETRY', + }, + { + type: 'linestring', + insertRaw: 'LINESTRING(POINT(1.2,-3.4),POINT(-5.6,10.23),POINT(0.2,0.7))', + expect: [ + { x: 1.2, y: -3.4 }, + { x: -5.6, y: 10.23 }, + { x: 0.2, y: 0.7 }, + ], + deep: true, + columnType: 'GEOMETRY', + }, + { + type: 'polygon', + insertRaw: `${stPrefix}GeomFromText('POLYGON((0 0,10 0,10 10,0 10,0 0),(5 5,7 5,7 7,5 7, 5 5))')`, + expect: [ + [ + { x: 0, y: 0 }, + { x: 10, y: 0 }, + { x: 10, y: 10 }, + { x: 0, y: 10 }, + { x: 0, y: 0 }, + ], + [ + { x: 5, y: 5 }, + { x: 7, y: 5 }, + { x: 7, y: 7 }, + { x: 5, y: 7 }, + { x: 5, y: 5 }, + ], + ], + deep: true, + columnType: 'GEOMETRY', + }, + { + type: 'geometry', + insertRaw: 'POINT(1.2,-3.4)', + expect: { x: 1.2, y: -3.4 }, + deep: true, + columnType: 'GEOMETRY', + }, + { + type: 'multipoint', + insertRaw: `${stPrefix}GeomFromText('MULTIPOINT(0 0, 20 20, 60 60)')`, + expect: [ + { x: 0, y: 0 }, + { x: 20, y: 20 }, + { x: 60, y: 60 }, + ], + deep: true, + columnType: 'GEOMETRY', + }, + { + type: 'multilinestring', + insertRaw: `${stPrefix}GeomFromText('MULTILINESTRING((10 10, 20 20), (15 15, 30 15))')`, + expect: [ + [ + { x: 10, y: 10 }, + { x: 20, y: 20 }, + ], + [ + { x: 15, y: 15 }, + { x: 30, y: 15 }, + ], + ], + deep: true, + columnType: 'GEOMETRY', + }, + { + type: 'multipolygon', + insertRaw: `${stPrefix}GeomFromText('MULTIPOLYGON(((0 0,10 0,10 10,0 10,0 0)),((5 5,7 5,7 7,5 7, 5 5)))')`, + expect: [ + [ + [ + { x: 0, y: 0 }, + { x: 10, y: 0 }, + { x: 10, y: 10 }, + { x: 0, y: 10 }, + { x: 0, y: 0 }, + ], + ], + [ + [ + { x: 5, y: 5 }, + { x: 7, y: 5 }, + { x: 7, y: 7 }, + { x: 5, y: 7 }, + { x: 5, y: 5 }, + ], + ], + ], + deep: true, + columnType: 'GEOMETRY', + }, + { + type: 'geometrycollection', + insertRaw: `${stPrefix}GeomFromText('GEOMETRYCOLLECTION(POINT(11 10), POINT(31 30), LINESTRING(15 15, 20 20))')`, + expect: [ + { x: 11, y: 10 }, + { x: 31, y: 30 }, + [ + { x: 15, y: 15 }, + { x: 20, y: 20 }, + ], + ], + deep: true, + columnType: 'GEOMETRY', + }, + ]; +} diff --git a/test/esm/integration/graceful-end/test-end-with-default-config.test.mts b/test/esm/integration/graceful-end/test-end-with-default-config.test.mts new file mode 100644 index 0000000000..4f262f16ad --- /dev/null +++ b/test/esm/integration/graceful-end/test-end-with-default-config.test.mts @@ -0,0 +1,34 @@ +import type { PoolConnection } from '../../../../index.js'; +import { assert } from 'poku'; +import { createPool } from '../../common.test.mjs'; + +/** + * This test case tests that the pool releases connections gracefully after the idle timeout has passed. + * + * @see https://github.com/sidorares/node-mysql2/issues/3148 + */ + +/** + * By default, the end method of a pooled connection will just release it back to the pool. + * This is compatibility behavior with mysqljs/mysql. + */ +const pool = createPool(); +let warningEmitted = false; + +pool.getConnection((_err1: Error | null, connection: PoolConnection) => { + connection.on('warn', (warning: Error) => { + warningEmitted = true; + assert( + warning.message.startsWith( + 'Calling conn.end() to release a pooled connection is deprecated' + ) + ); + }); + + connection.end(); + pool.end(); +}); + +process.on('exit', () => { + assert(warningEmitted, 'Warning should be emitted'); +}); diff --git a/test/esm/integration/graceful-end/test-end-with-graceful-end-config.test.mts b/test/esm/integration/graceful-end/test-end-with-graceful-end-config.test.mts new file mode 100644 index 0000000000..c5cf0d22dc --- /dev/null +++ b/test/esm/integration/graceful-end/test-end-with-graceful-end-config.test.mts @@ -0,0 +1,29 @@ +import type { PoolConnection } from '../../../../index.js'; +import { assert } from 'poku'; +import { createPool } from '../../common.test.mjs'; + +/** + * This test case tests that the pool releases connections gracefully after the idle timeout has passed. + * + * @see https://github.com/sidorares/node-mysql2/issues/3148 + */ + +/** + * By providing gracefulEnd when creating the pool, the end method of a pooled connection + * will actually close the connection instead of releasing it back to the pool. + */ +const pool = createPool({ gracefulEnd: true }); +let warningEmitted = false; + +pool.getConnection((_err1: Error | null, connection: PoolConnection) => { + connection.on('warn', () => { + warningEmitted = true; + }); + + connection.end(); + pool.end(); +}); + +process.on('exit', () => { + assert(!warningEmitted, 'Warning should not be emitted'); +}); diff --git a/test/esm/integration/graceful-end/test-pool-release-idle-with-default-config.test.mts b/test/esm/integration/graceful-end/test-pool-release-idle-with-default-config.test.mts new file mode 100644 index 0000000000..a6d3da964a --- /dev/null +++ b/test/esm/integration/graceful-end/test-pool-release-idle-with-default-config.test.mts @@ -0,0 +1,40 @@ +import type { PoolConnection } from '../../../../index.js'; +import { assert } from 'poku'; +import { createPool } from '../../common.test.mjs'; + +/** + * This test case tests that the pool releases connections gracefully after the idle timeout has passed. + * + * @see https://github.com/sidorares/node-mysql2/issues/3148 + */ + +const pool = createPool({ + connectionLimit: 2, + maxIdle: 1, + idleTimeout: 500, + debug: true, +}); + +let quitCommandReceived = false; +const originalLog = console.log; +console.log = (message: string) => { + if (message === 'Add command: Quit') { + quitCommandReceived = true; + } +}; + +pool.getConnection((_err1: Error | null, connection1: PoolConnection) => { + pool.getConnection((_err2: Error | null, connection2: PoolConnection) => { + connection1.release(); + connection2.release(); + + setTimeout(() => { + pool.end(); + }, 2000); + }); +}); + +process.on('exit', () => { + assert(!quitCommandReceived, 'quit command should not have been received'); + console.log = originalLog; +}); diff --git a/test/esm/integration/graceful-end/test-pool-release-idle-with-graceful-end-config.test.mts b/test/esm/integration/graceful-end/test-pool-release-idle-with-graceful-end-config.test.mts new file mode 100644 index 0000000000..b92d003b70 --- /dev/null +++ b/test/esm/integration/graceful-end/test-pool-release-idle-with-graceful-end-config.test.mts @@ -0,0 +1,40 @@ +import { assert } from 'poku'; +import { createPool } from '../../common.test.mjs'; + +/** + * This test case tests that the pool releases connections gracefully after the idle timeout has passed. + * + * @see https://github.com/sidorares/node-mysql2/issues/3148 + */ + +const pool = createPool({ + connectionLimit: 2, + maxIdle: 1, + idleTimeout: 500, + debug: true, + gracefulEnd: true, +}); + +let quitCommandReceived = false; +const originalLog = console.log; +console.log = (message: string) => { + if (message === 'Add command: Quit') { + quitCommandReceived = true; + } +}; + +pool.getConnection((_err1, connection1) => { + pool.getConnection((_err2, connection2) => { + connection1.release(); + connection2.release(); + + setTimeout(() => { + pool.end(); + }, 2000); + }); +}); + +process.on('exit', () => { + assert(quitCommandReceived, 'quit command should have been received'); + console.log = originalLog; +}); diff --git a/test/esm/integration/named-placeholders.test.mts b/test/esm/integration/named-placeholders.test.mts index 232fddb25e..f836285da1 100644 --- a/test/esm/integration/named-placeholders.test.mts +++ b/test/esm/integration/named-placeholders.test.mts @@ -1,7 +1,11 @@ // TODO: `namedPlaceholders` can't be disabled at query level -import { assert, it, describe } from 'poku'; +import type { RowDataPacket } from '../../../index.js'; +import type { Pool as PromisePool } from '../../../promise.js'; +import { assert, describe, it } from 'poku'; import { createConnection, createPool } from '../common.test.mjs'; +type ResultRow = RowDataPacket & { result: number }; + await describe('Test namedPlaceholder as command parameter in connection', async () => { const query = 'SELECT result FROM (SELECT 1 as result) temp WHERE temp.result=:named'; @@ -26,7 +30,7 @@ await describe('Test namedPlaceholder as command parameter in connection', async await it(async () => { const c = createConnection({ namedPlaceholders: false }).promise(); - const [rows] = await c.query( + const [rows] = await c.query( { sql: query, namedPlaceholders: true }, values ); @@ -58,7 +62,7 @@ await describe('Test namedPlaceholder as command parameter in connection', async await it(async () => { const c = createConnection({ namedPlaceholders: false }).promise(); - const [rows] = await c.execute( + const [rows] = await c.execute( { sql: query, namedPlaceholders: true }, values ); @@ -88,9 +92,9 @@ await describe('Test namedPlaceholder as command parameter in connection', async // }); await it(async () => { - const c = createPool({ namedPlaceholders: false }).promise(); + const c: PromisePool = createPool({ namedPlaceholders: false }).promise(); - const [rows] = await c.query( + const [rows] = await c.query( { sql: query, namedPlaceholders: true }, values ); @@ -120,9 +124,9 @@ await describe('Test namedPlaceholder as command parameter in connection', async // }); await it(async () => { - const c = createPool({ namedPlaceholders: false }).promise(); + const c: PromisePool = createPool({ namedPlaceholders: false }).promise(); - const [rows] = await c.execute( + const [rows] = await c.execute( { sql: query, namedPlaceholders: true }, values ); diff --git a/test/esm/integration/parsers/execute-results-creation.test.mts b/test/esm/integration/parsers/execute-results-creation.test.mts index 824d743ff0..21c157beaa 100644 --- a/test/esm/integration/parsers/execute-results-creation.test.mts +++ b/test/esm/integration/parsers/execute-results-creation.test.mts @@ -1,6 +1,9 @@ -import { it, describe, assert } from 'poku'; +import type { ResultSetHeader, RowDataPacket } from '../../../../index.js'; +import { assert, describe, it } from 'poku'; import { createConnection } from '../../common.test.mjs'; +type TestRow = RowDataPacket & { test: number; customProp?: boolean }; + await describe('Execute: Results Creation', async () => { const connection = createConnection().promise(); @@ -14,7 +17,9 @@ await describe('Execute: Results Creation', async () => { const proto = Object.getPrototypeOf(emptyObject); const privateObjectProps: string[] = Object.getOwnPropertyNames(proto); - const [results] = await connection.execute('SELECT 1+1 AS `test`'); + const [results] = await connection.execute( + 'SELECT 1+1 AS `test`' + ); assert.deepStrictEqual(results, expected, 'Ensure exact object "results"'); assert.deepStrictEqual( @@ -41,7 +46,7 @@ await describe('Execute: Results Creation', async () => { }); await it(async () => { - const [result] = await connection.execute('SET @1 = 1;'); + const [result] = await connection.execute('SET @1 = 1;'); assert.strictEqual( result.constructor.name, diff --git a/test/esm/integration/parsers/json-parse.test.mts b/test/esm/integration/parsers/json-parse.test.mts index e3d9b3a33e..8929ece390 100644 --- a/test/esm/integration/parsers/json-parse.test.mts +++ b/test/esm/integration/parsers/json-parse.test.mts @@ -1,11 +1,14 @@ -import { it, describe, assert } from 'poku'; +import type { RowDataPacket } from '../../../../index.js'; +import { assert, describe, it } from 'poku'; import { createConnection } from '../../common.test.mjs'; +type JsonRow = RowDataPacket & { json_result: { test: boolean } }; + await describe('JSON Parser', async () => { const connection = createConnection().promise(); await it(async () => { - const [result] = await connection.query( + const [result] = await connection.query( `SELECT CAST('{"test": true}' AS JSON) AS json_result` ); @@ -17,7 +20,7 @@ await describe('JSON Parser', async () => { }); await it(async () => { - const [result] = await connection.execute( + const [result] = await connection.execute( `SELECT CAST('{"test": true}' AS JSON) AS json_result` ); diff --git a/test/esm/integration/parsers/json-string.test.mts b/test/esm/integration/parsers/json-string.test.mts index bb6a7ebf0c..02ac01ca13 100644 --- a/test/esm/integration/parsers/json-string.test.mts +++ b/test/esm/integration/parsers/json-string.test.mts @@ -1,13 +1,16 @@ -import { it, describe, assert } from 'poku'; +import type { RowDataPacket } from '../../../../index.js'; +import { assert, describe, it } from 'poku'; import { createConnection } from '../../common.test.mjs'; +type JsonRow = RowDataPacket & { json_result: string }; + await describe('JSON String', async () => { const connection = createConnection({ jsonStrings: true, }).promise(); await it(async () => { - const [result] = await connection.query( + const [result] = await connection.query( `SELECT CAST('{"test": true}' AS JSON) AS json_result` ); @@ -19,7 +22,7 @@ await describe('JSON String', async () => { }); await it(async () => { - const [result] = await connection.execute( + const [result] = await connection.execute( `SELECT CAST('{"test": true}' AS JSON) AS json_result` ); diff --git a/test/esm/integration/parsers/query-results-creation.test.mts b/test/esm/integration/parsers/query-results-creation.test.mts index 2c238dfaff..9679bfe694 100644 --- a/test/esm/integration/parsers/query-results-creation.test.mts +++ b/test/esm/integration/parsers/query-results-creation.test.mts @@ -1,6 +1,9 @@ -import { it, describe, assert } from 'poku'; +import type { ResultSetHeader, RowDataPacket } from '../../../../index.js'; +import { assert, describe, it } from 'poku'; import { createConnection } from '../../common.test.mjs'; +type TestRow = RowDataPacket & { test: number; customProp?: boolean }; + await describe('Query: Results Creation', async () => { const connection = createConnection().promise(); @@ -14,7 +17,7 @@ await describe('Query: Results Creation', async () => { const proto = Object.getPrototypeOf(emptyObject); const privateObjectProps: string[] = Object.getOwnPropertyNames(proto); - const [results] = await connection.query('SELECT 1+1 AS `test`'); + const [results] = await connection.query('SELECT 1+1 AS `test`'); assert.deepStrictEqual(results, expected, 'Ensure exact object "results"'); assert.deepStrictEqual( @@ -41,7 +44,7 @@ await describe('Query: Results Creation', async () => { }); await it(async () => { - const [result] = await connection.query('SET @1 = 1;'); + const [result] = await connection.query('SET @1 = 1;'); assert.strictEqual( result.constructor.name, diff --git a/test/esm/integration/parsers/typecast-field-datetime.test.mts b/test/esm/integration/parsers/typecast-field-datetime.test.mts index 77cf6c1796..9965dee00d 100644 --- a/test/esm/integration/parsers/typecast-field-datetime.test.mts +++ b/test/esm/integration/parsers/typecast-field-datetime.test.mts @@ -1,7 +1,9 @@ -import { describe, it, assert } from 'poku'; -import type { TypeCastField } from '../../../../index.js'; +import type { RowDataPacket, TypeCastField } from '../../../../index.js'; +import { assert, describe, it } from 'poku'; import { createConnection } from '../../common.test.mjs'; +type DateRow = RowDataPacket & { datetime: string | null }; + await describe('typeCast field.datetime', async () => { const conn = createConnection({ typeCast: (field: TypeCastField) => field.string(), @@ -19,13 +21,15 @@ await describe('typeCast field.datetime', async () => { ); await it('execute results', async () => { - const [date] = await conn.execute('SELECT datetime from tmp_date'); + const [date] = await conn.execute( + 'SELECT datetime from tmp_date' + ); execute.date = date[0].datetime; }); await it('query results', async () => { - const [date] = await conn.query('SELECT datetime from tmp_date'); + const [date] = await conn.query('SELECT datetime from tmp_date'); query.date = date[0].datetime; }); diff --git a/test/esm/integration/parsers/typecast-field-string.test.mts b/test/esm/integration/parsers/typecast-field-string.test.mts index 25bd28752f..5872662f6c 100644 --- a/test/esm/integration/parsers/typecast-field-string.test.mts +++ b/test/esm/integration/parsers/typecast-field-string.test.mts @@ -1,7 +1,16 @@ -import { describe, it, assert } from 'poku'; -import type { TypeCastField } from '../../../../index.js'; +import type { RowDataPacket, TypeCastField } from '../../../../index.js'; +import { assert, describe, it } from 'poku'; import { createConnection } from '../../common.test.mjs'; +type DateRow = RowDataPacket & { date: string | null }; +type TimeRow = RowDataPacket & { time: string | null }; +type DatetimeRow = RowDataPacket & { datetime: string | null }; +type TimestampRow = RowDataPacket & { timestamp: string | null }; +type TinyRow = RowDataPacket & { + signed: string | null; + unsigned: string | null; +}; + await describe('typeCast field.string', async () => { const conn = createConnection({ typeCast: (field: TypeCastField) => field.string(), @@ -29,17 +38,19 @@ await describe('typeCast field.string', async () => { ); await it('query results', async () => { - const [date] = await conn.query( + const [date] = await conn.query( 'SELECT STR_TO_DATE("2022-06-28", "%Y-%m-%d") AS `date`' ); - const [time] = await conn.query( + const [time] = await conn.query( 'SELECT STR_TO_DATE("12:34:56", "%H:%i:%s") AS `time`' ); - const [datetime] = await conn.query( + const [datetime] = await conn.query( 'SELECT STR_TO_DATE("2022-06-28 12:34:56", "%Y-%m-%d %H:%i:%s") AS `datetime`' ); - const [timestamp] = await conn.query('SELECT `timestamp` FROM `tmp_date`'); - const [tiny] = await conn.query( + const [timestamp] = await conn.query( + 'SELECT `timestamp` FROM `tmp_date`' + ); + const [tiny] = await conn.query( 'SELECT `signed`, `unsigned` FROM `tmp_tiny`' ); @@ -51,19 +62,19 @@ await describe('typeCast field.string', async () => { }); await it('execute results', async () => { - const [date] = await conn.execute( + const [date] = await conn.execute( 'SELECT STR_TO_DATE("2022-06-28", "%Y-%m-%d") AS `date`' ); - const [time] = await conn.execute( + const [time] = await conn.execute( 'SELECT STR_TO_DATE("12:34:56", "%H:%i:%s") AS `time`' ); - const [datetime] = await conn.execute( + const [datetime] = await conn.execute( 'SELECT STR_TO_DATE("2022-06-28 12:34:56", "%Y-%m-%d %H:%i:%s") AS `datetime`' ); - const [timestamp] = await conn.execute( + const [timestamp] = await conn.execute( 'SELECT `timestamp` FROM `tmp_date`' ); - const [tiny] = await conn.execute( + const [tiny] = await conn.execute( 'SELECT `signed`, `unsigned` FROM `tmp_tiny`' ); diff --git a/test/esm/integration/pool-cluster/test-promise-wrapper.test.mts b/test/esm/integration/pool-cluster/test-promise-wrapper.test.mts index 8a640a64e6..b7a2d9610d 100644 --- a/test/esm/integration/pool-cluster/test-promise-wrapper.test.mts +++ b/test/esm/integration/pool-cluster/test-promise-wrapper.test.mts @@ -1,8 +1,10 @@ -import { it, assert, describe } from 'poku'; -import type { QueryError } from '../../../../index.js'; +import type { QueryError, RowDataPacket } from '../../../../index.js'; +import { assert, describe, it } from 'poku'; import promiseDriver from '../../../../promise.js'; import { config } from '../../common.test.mjs'; +type TestRow = RowDataPacket & { a: number }; + const { createPoolCluster } = promiseDriver; await describe('Test pool cluster', async () => { @@ -100,13 +102,13 @@ await describe('Test pool cluster', async () => { assert.ok(connection, 'should get connection'); connection.release(); - const [result] = await poolNamespace.query( + const [result] = await poolNamespace.query( 'SELECT 1 as a from dual where 1 = ?', [1] ); assert.equal(result[0]['a'], 1, 'should query successfully'); - const [result2] = await poolNamespace.execute( + const [result2] = await poolNamespace.execute( 'SELECT 1 as a from dual where 1 = ?', [1] ); diff --git a/test/esm/integration/promise-wrappers/test-async-stack.test.mts b/test/esm/integration/promise-wrappers/test-async-stack.test.mts new file mode 100644 index 0000000000..f5dd650949 --- /dev/null +++ b/test/esm/integration/promise-wrappers/test-async-stack.test.mts @@ -0,0 +1,56 @@ +import type { ConnectionOptions } from '../../../../index.js'; +import process from 'node:process'; +import ErrorStackParser from 'error-stack-parser'; +import { assert } from 'poku'; +import { createConnection as promiseCreateConnection } from '../../../../promise.js'; +import { config } from '../../common.test.mjs'; + +// Uncaught Error: connect ECONNREFUSED 127.0.0.1:33066 - Local (undefined:undefined) +if (typeof Deno !== 'undefined') process.exit(0); + +const createConnection = async function (args?: ConnectionOptions) { + if (!args && process.env.MYSQL_CONNECTION_URL) { + return promiseCreateConnection({ uri: process.env.MYSQL_CONNECTION_URL }); + } + return promiseCreateConnection({ ...config, ...args }); +}; + +async function test() { + // TODO check this is actially required. This meant as a help for pre async/await node + // to load entire file and do isAsyncSupported check instead of failing with syntax error + + let e1: Error, e2: Error; + + // TODO: investigate why connection is still open after ENETUNREACH + async function test1() { + e1 = new Error(); + // expected not to connect + await createConnection({ host: '127.0.0.1', port: 33066 }); + } + + async function test2() { + const conn = await createConnection(); + try { + e2 = new Error(); + await Promise.all([conn.query('select 1+1'), conn.query('syntax error')]); + } catch (err) { + const stack = ErrorStackParser.parse(err as Error); + const stackExpected = ErrorStackParser.parse(e2); + assert( + stack[1].getLineNumber() === (stackExpected[0].getLineNumber() ?? 0) + 1 + ); + conn.end(); + } + } + + test1().catch((err) => { + const stack = ErrorStackParser.parse(err); + const stackExpected = ErrorStackParser.parse(e1); + assert( + stack[2].getLineNumber() === (stackExpected[0].getLineNumber() ?? 0) + 2 + ); + test2(); + }); +} + +test(); diff --git a/test/esm/integration/promise-wrappers/test-promise-wrappers.test.mts b/test/esm/integration/promise-wrappers/test-promise-wrappers.test.mts new file mode 100644 index 0000000000..03b538e13f --- /dev/null +++ b/test/esm/integration/promise-wrappers/test-promise-wrappers.test.mts @@ -0,0 +1,537 @@ +import type { RowDataPacket } from '../../../../index.js'; +import type { Connection } from '../../../../promise.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { + createPool as createPoolPromise, + createConnection as promiseCreateConnection, +} from '../../../../promise.js'; +import { config } from '../../common.test.mjs'; + +type TttRow = RowDataPacket & { ttt: number }; +type QqqRow = RowDataPacket & { qqq: number }; +type TttUuuRow = RowDataPacket & { ttt: number; uuu: string }; +type CurrentUserRow = RowDataPacket & { 'current_user()': string }; + +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test for planetscale'); + process.exit(0); +} + +const createConnection = promiseCreateConnection; +const createPool = createPoolPromise; + +// it's lazy exported from main index.js as well. Test that it's same function +const mainModule = await import('../../../../index.js'); +// @ts-expect-error: TODO: implement typings +const mainExport = mainModule.default.createConnectionPromise; +assert.equal(mainExport, createConnection); + +let doneCalled = false; +let exceptionCaught = false; +let doneEventsConnect = false; + +let doneCalledPool = false; +let exceptionCaughtPool = false; +let doneEventsPool = false; +let doneChangeUser = false; + +function testBasic() { + let connResolved: Connection | undefined; + createConnection(config) + .then((conn) => { + connResolved = conn; + return conn.query('select 1+2 as ttt'); + }) + .then((result1) => { + assert.equal(result1[0][0].ttt, 3); + return connResolved?.query('select 2+2 as qqq'); + }) + .then((result2) => { + assert.equal(result2?.[0][0].qqq, 4); + return connResolved?.end(); + }) + .then(() => { + doneCalled = true; + }) + .catch((err) => { + throw err; + }); +} + +function testErrors() { + let connResolved: Connection | undefined; + const connPromise = createConnection(config); + + connPromise + .then((conn) => { + connResolved = conn; + return conn.query('select 1+2 as ttt'); + }) + .then((result1) => { + assert.equal(result1[0][0].ttt, 3); + return connResolved?.query('bad sql'); + }) + .then((result2) => { + assert.equal(result2?.[0][0].ttt, 3); + return connResolved?.query('select 2+2 as qqq'); + }) + .catch(() => { + exceptionCaught = true; + if (connResolved) { + connResolved.end(); + } else { + console.log('Warning: promise rejected before first query'); + } + }); +} + +function testObjParams() { + let connResolved: Connection | undefined; + createConnection(config) + .then((conn) => { + connResolved = conn; + return conn.query({ + sql: 'select ?-? as ttt', + values: [5, 2], + }); + }) + .then((result1) => { + assert.equal(result1[0][0].ttt, 3); + return connResolved?.execute({ + sql: 'select ?-? as ttt', + values: [8, 5], + }); + }) + .then((result2) => { + assert.equal(result2?.[0][0].ttt, 3); + return connResolved?.end(); + }) + .catch((err) => { + console.log(err); + }); +} + +function testPrepared() { + let connResolved: Connection | undefined; + createConnection(config) + .then((conn) => { + connResolved = conn; + return conn.prepare('select ?-? as ttt, ? as uuu'); + }) + .then((statement) => statement.execute([11, 3, 'test'])) + .then((result) => { + const rows = result[0] as TttUuuRow[]; + assert.equal(rows[0].ttt, 8); + assert.equal(rows[0].uuu, 'test'); + return connResolved?.end(); + }) + .catch((err) => { + console.log(err); + if (connResolved) { + connResolved.end(); + } else { + console.log( + 'Warning: promise rejected before executing prepared statement' + ); + } + }); +} + +// REVIEW: Unused + +function testEventsConnect() { + let connResolved: Connection | undefined; + createConnection(config) + .then((conn) => { + connResolved = conn; + let events = 0; + + const expectedListeners: Record = { + error: 1, + drain: 0, + connect: 0, + enqueue: 0, + end: 0, + }; + for (const eventName in expectedListeners) { + assert.equal( + // @ts-expect-error: TODO: implement typings + conn.connection.listenerCount(eventName), + expectedListeners[eventName], + eventName + ); + } + + conn + .once( + 'error', + function () { + assert.equal(this, conn); + ++events; + }.bind(conn) + ) + .once( + 'drain', + function () { + assert.equal(this, conn); + ++events; + }.bind(conn) + ) + .once( + 'connect', + function () { + assert.equal(this, conn); + ++events; + }.bind(conn) + ) + .once( + 'enqueue', + function () { + assert.equal(this, conn); + ++events; + }.bind(conn) + ) + .once( + 'end', + function () { + assert.equal(this, conn); + ++events; + + doneEventsConnect = events === 5; + }.bind(conn) + ); + + // @ts-expect-error: TODO: implement typings + conn.connection.emit('error', new Error()); + // @ts-expect-error: TODO: implement typings + conn.connection.emit('drain'); + // @ts-expect-error: TODO: implement typings + conn.connection.emit('connect'); + // @ts-expect-error: TODO: implement typings + conn.connection.emit('enqueue'); + // @ts-expect-error: TODO: implement typings + conn.connection.emit('end'); + + expectedListeners.error = 0; + for (const eventName in expectedListeners) { + assert.equal( + // @ts-expect-error: TODO: implement typings + conn.connection.listenerCount(eventName), + expectedListeners[eventName], + eventName + ); + } + + conn.end(); + }) + .catch((err) => { + console.log(err); + if (connResolved) { + connResolved.end(); + } else { + console.log( + 'Warning: promise rejected before executing prepared statement' + ); + } + }); +} + +function testBasicPool() { + const pool = createPool(config); + + pool + .getConnection() + .then((connResolved) => { + pool.releaseConnection(connResolved); + return pool.query('select 1+2 as ttt'); + }) + .then((result1) => { + assert.equal(result1[0][0].ttt, 3); + return pool.query('select 2+2 as qqq'); + }) + .then((result2) => { + assert.equal(result2[0][0].qqq, 4); + return pool.end(); + }) + .then(() => { + doneCalledPool = true; + }) + .catch((err) => { + throw err; + }); +} + +function testErrorsPool() { + const pool = createPool(config); + pool + .query('select 1+2 as ttt') + .then((result1) => { + assert.equal(result1[0][0].ttt, 3); + return pool.query('bad sql'); + }) + .then((result2) => { + assert.equal(result2[0][0].ttt, 3); + return pool.query('select 2+2 as qqq'); + }) + .catch(() => { + exceptionCaughtPool = true; + return pool.end(); + }); +} + +function testObjParamsPool() { + const pool = createPool(config); + pool + .query({ + sql: 'select ?-? as ttt', + values: [5, 2], + }) + .then((result1) => { + assert.equal(result1[0][0].ttt, 3); + return pool.execute({ + sql: 'select ?-? as ttt', + values: [8, 5], + }); + }) + .then((result2) => { + assert.equal(result2[0][0].ttt, 3); + return pool.end(); + }) + .catch((err) => { + console.log(err); + }); +} +function testPromiseLibrary() { + const pool = createPool(config); + let promise: Promise = pool.execute({ + sql: 'select ?-? as ttt', + values: [8, 5], + }); + promise + .then(() => { + // @ts-expect-error: TODO: implement typings + assert.ok(promise instanceof pool.Promise); + }) + .then(() => { + promise = pool.end(); + // @ts-expect-error: TODO: implement typings + assert.ok(promise instanceof pool.Promise); + }) + .catch((err) => { + console.log(err); + }); +} + +function testEventsPool() { + const pool = createPool(config); + let events = 0; + + const expectedListeners: Record = { + acquire: 0, + connection: 0, + enqueue: 0, + release: 0, + }; + for (const eventName in expectedListeners) { + assert.equal( + pool.pool.listenerCount(eventName), + expectedListeners[eventName], + eventName + ); + } + + pool + .once( + 'acquire', + function () { + assert.equal(this, pool); + ++events; + }.bind(pool) + ) + .once( + 'connection', + function () { + assert.equal(this, pool); + ++events; + }.bind(pool) + ) + .once( + 'enqueue', + function () { + assert.equal(this, pool); + ++events; + }.bind(pool) + ) + .once( + 'release', + function () { + assert.equal(this, pool); + ++events; + + doneEventsPool = events === 4; + }.bind(pool) + ); + + pool.pool.emit('acquire'); + pool.pool.emit('connection'); + pool.pool.emit('enqueue'); + pool.pool.emit('release'); + + for (const eventName in expectedListeners) { + assert.equal( + pool.pool.listenerCount(eventName), + expectedListeners[eventName], + eventName + ); + } +} + +function testChangeUser() { + const onlyUsername = function (name: string) { + return name.substring(0, name.indexOf('@')); + }; + let connResolved: Connection | undefined; + + createConnection(config) + .then((conn) => { + connResolved = conn; + return connResolved.query( + "CREATE USER IF NOT EXISTS 'changeuser1'@'%' IDENTIFIED BY 'changeuser1pass'" + ); + }) + .then(() => { + connResolved?.query( + "CREATE USER IF NOT EXISTS 'changeuser2'@'%' IDENTIFIED BY 'changeuser2pass'" + ); + connResolved?.query("GRANT ALL ON *.* TO 'changeuser1'@'%'"); + connResolved?.query("GRANT ALL ON *.* TO 'changeuser2'@'%'"); + return connResolved?.query('FLUSH PRIVILEGES'); + }) + .then(() => { + connResolved?.changeUser({ + user: 'changeuser1', + password: 'changeuser1pass', + }); + }) + .then(() => connResolved?.query('select current_user()')) + .then((result) => { + const rows = result?.[0]; + assert.deepEqual( + onlyUsername(rows?.[0]['current_user()'] ?? ''), + 'changeuser1' + ); + return connResolved?.changeUser({ + user: 'changeuser2', + password: 'changeuser2pass', + }); + }) + .then(() => connResolved?.query('select current_user()')) + .then((result) => { + const rows = result?.[0]; + assert.deepEqual( + onlyUsername(rows?.[0]['current_user()'] ?? ''), + 'changeuser2' + ); + return connResolved?.changeUser({ + user: 'changeuser1', + // TODO: re-enable testing passwordSha1 auth. Only works for mysql_native_password, so need to change test to create user with this auth method + password: 'changeuser1pass', + //passwordSha1: Buffer.from('f961d39c82138dcec42b8d0dcb3e40a14fb7e8cd', 'hex') // sha1(changeuser1pass) + }); + }) + .then(() => connResolved?.query('select current_user()')) + .then((result) => { + const rows = result?.[0]; + assert.deepEqual( + onlyUsername(rows?.[0]['current_user()'] ?? ''), + 'changeuser1' + ); + doneChangeUser = true; + return connResolved?.end(); + }) + .catch((err) => { + console.log(err); + if (connResolved) { + connResolved.end(); + } + throw err; + }); +} + +function testConnectionProperties() { + let connResolved: Connection | undefined; + createConnection(config) + .then((conn) => { + connResolved = conn; + assert.equal(typeof conn.config, 'object'); + assert.ok('queryFormat' in conn.config); + assert.equal(typeof conn.threadId, 'number'); + return connResolved.end(); + }) + .catch((err) => { + if (connResolved) { + connResolved.end(); + } + throw err; + }); +} + +function timebomb(fuse: number) { + let timebomb: ReturnType | undefined; + + return { + arm() { + timebomb = setTimeout(() => { + throw new Error(`Timebomb not defused within ${fuse}ms`); + }, fuse); + }, + defuse() { + clearTimeout(timebomb); + }, + }; +} + +function testPoolConnectionDestroy() { + // Only allow one connection + const options = Object.assign({ connectionLimit: 1 }, config); + const pool = createPool(options); + + const bomb = timebomb(2000); + + pool + .getConnection() + .then((connection) => connection.destroy()) + .then(bomb.arm) + .then(() => pool.getConnection()) + .then(bomb.defuse) + .then(() => pool.end()); +} + +testBasic(); +testErrors(); +testObjParams(); +testPrepared(); +testEventsConnect(); +testBasicPool(); +testErrorsPool(); +testObjParamsPool(); +testEventsPool(); +testChangeUser(); +testConnectionProperties(); +testPoolConnectionDestroy(); +testPromiseLibrary(); + +process.on('exit', () => { + assert.equal(doneCalled, true, 'done not called'); + assert.equal(exceptionCaught, true, 'exception not caught'); + assert.equal(doneEventsConnect, true, 'wrong number of connection events'); + assert.equal(doneCalledPool, true, 'pool done not called'); + assert.equal(exceptionCaughtPool, true, 'pool exception not caught'); + assert.equal(doneEventsPool, true, 'wrong number of pool connection events'); + assert.equal(doneChangeUser, true, 'user not changed'); +}); + +process.on('unhandledRejection', (err) => { + console.log('error:', (err as Error).stack); +}); diff --git a/test/esm/integration/regressions/test-#433.test.mts b/test/esm/integration/regressions/test-#433.test.mts new file mode 100644 index 0000000000..b953dc77e0 --- /dev/null +++ b/test/esm/integration/regressions/test-#433.test.mts @@ -0,0 +1,94 @@ +import type { QueryError, RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +// TODO: reach out to PlanetScale to clarify charset support +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test for planetscale'); + process.exit(0); +} + +const connection = createConnection({ charset: 'KOI8R_GENERAL_CI' }); + +const tableName = 'МояТаблица'; +const testFields = ['поле1', 'поле2', 'поле3', 'поле4']; +const testRows = [ + ['привет', 'мир', 47, 7], + ['ура', 'тест', 11, 108], +]; + +let actualRows: RowDataPacket[] = []; +let actualError = ''; + +function executeErrorMessageTest() { + // tableName does not have closing "`", we do this to have tableName in error string + // it is sent back in original encoding (koi8r), we are testing that it's decoded correctly + connection.query(`SELECT * FROM \`${tableName}`, (err) => { + if (!err) { + assert.fail('Expected query to fail'); + } + actualError = err.message; + connection.end(); + }); +} + +function executeTest(err: QueryError | null) { + assert.ifError(err); + connection.query( + `SELECT * FROM \`${tableName}\``, + (err, rows) => { + assert.ifError(err); + actualRows = rows; + executeErrorMessageTest(); + } + ); +} + +connection.query( + [ + `CREATE TEMPORARY TABLE \`${tableName}\` (`, + ` \`${testFields[0]}\` varchar(255) NOT NULL,`, + ` \`${testFields[1]}\` varchar(255) NOT NULL,`, + ` \`${testFields[2]}\` int(11) NOT NULL,`, + ` \`${testFields[3]}\` int(11) NOT NULL,`, + ` PRIMARY KEY (\`${testFields[0]}\`)`, + ') ENGINE=InnoDB DEFAULT CHARSET=utf8', + ].join(' '), + (err) => { + assert.ifError(err); + connection.query( + [ + `INSERT INTO \`${tableName}\` VALUES`, + `("${testRows[0][0]}","${testRows[0][1]}", ${testRows[0][2]}, ${testRows[0][3]}),`, + `("${testRows[1][0]}","${testRows[1][1]}", ${testRows[1][2]}, ${testRows[1][3]})`, + ].join(' '), + executeTest + ); + } +); + +/* eslint quotes: 0 */ +const expectedErrorMysql = + "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '`МояТаблица' at line 1"; + +const expectedErrorMariaDB = + "You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '`МояТаблица' at line 1"; + +process.on('exit', () => { + testRows.map((tRow, index) => { + const cols = testFields; + const aRow = actualRows[index]; + assert.equal(aRow[cols[0]], tRow[0]); + assert.equal(aRow[cols[1]], tRow[1]); + assert.equal(aRow[cols[2]], tRow[2]); + assert.equal(aRow[cols[3]], tRow[3]); + }); + + // @ts-expect-error: internal access + if (connection._handshakePacket.serverVersion.match(/MariaDB/)) { + assert.equal(actualError, expectedErrorMariaDB); + } else { + assert.equal(actualError, expectedErrorMysql); + } +}); diff --git a/test/esm/integration/regressions/test-#442.test.mts b/test/esm/integration/regressions/test-#442.test.mts new file mode 100644 index 0000000000..c603855f36 --- /dev/null +++ b/test/esm/integration/regressions/test-#442.test.mts @@ -0,0 +1,61 @@ +import type { QueryError, RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +const tableName = '商城'; +const testFields = ['商品类型', '商品说明', '价格', '剩余']; +const testRows = [ + ['商类型', '商品型', 47, 7], + ['类商型', '商型品', 11, 108], +]; + +let actualRows: RowDataPacket[] = []; + +function executeTest(err: QueryError | null) { + assert.ifError(err); + connection.query( + `SELECT * FROM \`${tableName}\``, + (err, rows) => { + assert.ifError(err); + actualRows = rows; + connection.end(); + } + ); +} + +connection.query( + [ + `CREATE TEMPORARY TABLE \`${tableName}\` (`, + ` \`${testFields[0]}\` varchar(255) NOT NULL,`, + ` \`${testFields[1]}\` varchar(255) NOT NULL,`, + ` \`${testFields[2]}\` int(11) NOT NULL,`, + ` \`${testFields[3]}\` int(11) NOT NULL,`, + ` PRIMARY KEY (\`${testFields[0]}\`)`, + ') ENGINE=InnoDB DEFAULT CHARSET=utf8', + ].join(' '), + (err) => { + assert.ifError(err); + connection.query( + [ + `INSERT INTO \`${tableName}\` VALUES`, + `("${testRows[0][0]}","${testRows[0][1]}", ${testRows[0][2]}, ${testRows[0][3]}),`, + `("${testRows[1][0]}","${testRows[1][1]}", ${testRows[1][2]}, ${testRows[1][3]})`, + ].join(' '), + executeTest + ); + } +); + +process.on('exit', () => { + testRows.map((tRow, index) => { + const cols = testFields; + const aRow = actualRows[index]; + assert.equal(aRow[cols[0]], tRow[0]); + assert.equal(aRow[cols[1]], tRow[1]); + assert.equal(aRow[cols[2]], tRow[2]); + assert.equal(aRow[cols[3]], tRow[3]); + }); +}); diff --git a/test/esm/integration/regressions/test-#485.test.mts b/test/esm/integration/regressions/test-#485.test.mts new file mode 100644 index 0000000000..17811de459 --- /dev/null +++ b/test/esm/integration/regressions/test-#485.test.mts @@ -0,0 +1,42 @@ +import type { PoolOptions, RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import PoolConnection from '../../../../lib/pool_connection.js'; +import { createPool as createPoolPromise } from '../../../../promise.js'; +import { config } from '../../common.test.mjs'; + +type TestRow = RowDataPacket & { ttt: number }; + +function createPool(args?: PoolOptions) { + if (!args && process.env.MYSQL_CONNECTION_URL) { + return createPoolPromise({ uri: process.env.MYSQL_CONNECTION_URL }); + } + return createPoolPromise({ ...config, ...args }); +} + +// stub +const release = PoolConnection.prototype.release; +let releaseCalls = 0; +PoolConnection.prototype.release = function () { + releaseCalls++; +}; + +function testPoolPromiseExecuteLeak() { + const pool = createPool(); + pool + .execute('select 1+2 as ttt') + .then((result) => { + assert.equal(result[0][0].ttt, 3); + return pool.end(); + }) + .catch((err) => { + assert.ifError(err); + }); +} + +testPoolPromiseExecuteLeak(); + +process.on('exit', () => { + PoolConnection.prototype.release = release; + assert.equal(releaseCalls, 1, 'PoolConnection.release was not called'); +}); diff --git a/test/esm/integration/regressions/test-#617.test.mts b/test/esm/integration/regressions/test-#617.test.mts new file mode 100644 index 0000000000..02ac1f21d7 --- /dev/null +++ b/test/esm/integration/regressions/test-#617.test.mts @@ -0,0 +1,77 @@ +import type { QueryError, RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +// PlanetScale response has trailing 000 in 2017-07-26 09:36:42.000 +// TODO: rewrite test to account for variations. Skipping for now on PS +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test for planetscale'); + process.exit(0); +} + +const connection = createConnection({ dateStrings: true }); + +const tableName = 'dates'; +const testFields = ['id', 'date', 'name']; +const testRows = [ + [1, '2017-07-26 09:36:42.000', 'John'], + [2, '2017-07-26 09:36:42.123', 'Jane'], +]; +const expected = [ + { + id: 1, + date: '2017-07-26 09:36:42', + name: 'John', + }, + { + id: 2, + date: '2017-07-26 09:36:42.123', + name: 'Jane', + }, +]; + +let actualRows: RowDataPacket[] = []; + +function executeTest(err: QueryError | null) { + assert.ifError(err); + connection.execute( + `SELECT * FROM \`${tableName}\``, + (err, rows) => { + assert.ifError(err); + actualRows = rows; + connection.end(); + } + ); +} + +connection.query( + [ + `CREATE TEMPORARY TABLE \`${tableName}\` (`, + ` \`${testFields[0]}\` int,`, + ` \`${testFields[1]}\` TIMESTAMP(3),`, + ` \`${testFields[2]}\` varchar(10)`, + ') ENGINE=InnoDB DEFAULT CHARSET=utf8', + ].join(' '), + (err) => { + assert.ifError(err); + connection.query( + [ + `INSERT INTO \`${tableName}\` VALUES`, + `(${testRows[0][0]},"${testRows[0][1]}", "${testRows[0][2]}"),`, + `(${testRows[1][0]},"${testRows[1][1]}", "${testRows[1][2]}")`, + ].join(' '), + executeTest + ); + } +); + +process.on('exit', () => { + console.log(actualRows); + expected.map((exp, index) => { + const row = actualRows[index]; + Object.keys(exp).map((key) => { + assert.equal(exp[key as keyof typeof exp], row[key]); + }); + }); +}); diff --git a/test/esm/integration/regressions/test-#629.test.mts b/test/esm/integration/regressions/test-#629.test.mts new file mode 100644 index 0000000000..1c7a568582 --- /dev/null +++ b/test/esm/integration/regressions/test-#629.test.mts @@ -0,0 +1,79 @@ +import type { QueryError, RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection({ + dateStrings: false, + timezone: 'Z', +}); + +const tableName = 'dates'; +const testFields = ['id', 'date1', 'date2', 'name']; +const testRows = [ + [1, '2017-07-26 09:36:42.000', '2017-07-29 09:22:24.000', 'John'], + [2, '2017-07-26 09:36:42.123', '2017-07-29 09:22:24.321', 'Jane'], +]; +const expected = [ + { + id: 1, + date1: new Date('2017-07-26T09:36:42.000Z'), + date2: new Date('2017-07-29T09:22:24.000Z'), + name: 'John', + }, + { + id: 2, + date1: new Date('2017-07-26T09:36:42.123Z'), + date2: new Date('2017-07-29T09:22:24.321Z'), + name: 'Jane', + }, +]; + +let actualRows: RowDataPacket[] = []; + +function executeTest(err: QueryError | null) { + assert.ifError(err); + connection.execute( + `SELECT * FROM \`${tableName}\``, + (err, rows) => { + assert.ifError(err); + actualRows = rows; + connection.end(); + } + ); +} + +connection.query( + [ + `CREATE TEMPORARY TABLE \`${tableName}\` (`, + ` \`${testFields[0]}\` int,`, + ` \`${testFields[1]}\` TIMESTAMP(3),`, + ` \`${testFields[2]}\` DATETIME(3),`, + ` \`${testFields[3]}\` varchar(10)`, + ') ENGINE=InnoDB DEFAULT CHARSET=utf8', + ].join(' '), + (err) => { + assert.ifError(err); + connection.query( + [ + `INSERT INTO \`${tableName}\` VALUES`, + `(${testRows[0][0]},"${testRows[0][1]}", "${testRows[0][2]}", "${testRows[0][3]}"),`, + `(${testRows[1][0]},"${testRows[1][1]}", "${testRows[1][2]}", "${testRows[1][3]}")`, + ].join(' '), + executeTest + ); + } +); + +process.on('exit', () => { + expected.map((exp, index) => { + const row = actualRows[index]; + Object.keys(exp).map((key) => { + if (key.startsWith('date')) { + assert.equal(+exp[key as keyof typeof exp], +row[key]); + } else { + assert.equal(exp[key as keyof typeof exp], row[key]); + } + }); + }); +}); diff --git a/test/esm/integration/regressions/test-#82.test.mts b/test/esm/integration/regressions/test-#82.test.mts new file mode 100644 index 0000000000..e051f68766 --- /dev/null +++ b/test/esm/integration/regressions/test-#82.test.mts @@ -0,0 +1,63 @@ +import type { QueryError, RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const connection = createConnection(); + +const config = { + table1: 'test82t1', + table2: 'test82t2', + view1: 'view82v1', + view2: 'view82v2', +}; +let results: RowDataPacket[] = []; + +const prepareTestSet = function (cb: (err: QueryError | null) => void) { + connection.query(`drop table if exists ${config.table1}`); + connection.query(`drop table if exists ${config.table2}`); + connection.query(`drop view if exists ${config.view1}`); + connection.query(`drop view if exists ${config.view2}`); + connection.query( + `create table ${config.table1} (name1 varchar(20), linkId1 integer(11))` + ); + connection.query( + `create table ${config.table2} (name2 varchar(20), linkId2 integer(11))` + ); + connection.query( + `insert into ${config.table1} (name1, linkId1) values ("A", 1),("B", 2),("C", 3),("D", 4)` + ); + connection.query( + `insert into ${config.table2} (name2, linkId2) values ("AA", 1),("BB", 2),("CC", 3),("DD", 4)` + ); + connection.query( + `create view ${config.view1} as select name1, linkId1, name2 from ${config.table1} INNER JOIN ${config.table2} ON linkId1 = linkId2` + ); + connection.query( + `create view ${config.view2} as select name1, name2 from ${config.view1}`, + cb + ); +}; + +prepareTestSet((err) => { + assert.ifError(err); + connection.query( + `select * from ${config.view2} order by name2 desc`, + (err, rows) => { + assert.ifError(err); + results = rows; + connection.end(); + } + ); +}); + +process.on('exit', () => { + assert.equal(results[0].name1, 'D'); + assert.equal(results[1].name1, 'C'); + assert.equal(results[2].name1, 'B'); + assert.equal(results[3].name1, 'A'); + assert.equal(results[0].name2, 'DD'); + assert.equal(results[1].name2, 'CC'); + assert.equal(results[2].name2, 'BB'); + assert.equal(results[3].name2, 'AA'); +}); diff --git a/test/esm/integration/test-auth-switch-multi-factor.test.mts b/test/esm/integration/test-auth-switch-multi-factor.test.mts new file mode 100644 index 0000000000..cef51b4ff5 --- /dev/null +++ b/test/esm/integration/test-auth-switch-multi-factor.test.mts @@ -0,0 +1,169 @@ +// Copyright (c) 2021, Oracle and/or its affiliates. + +import type { Connection } from '../../../index.js'; +import { Buffer } from 'node:buffer'; +import process from 'node:process'; +import { assert } from 'poku'; +import portfinder from 'portfinder'; +import mysql from '../../../index.js'; +import Command from '../../../lib/commands/command.js'; +import Packets from '../../../lib/packets/index.js'; + +type AuthSwitchArgs = { + pluginName: string; + pluginData: Buffer; +}; + +// The process is not terminated in Deno +if (typeof Deno !== 'undefined') process.exit(0); + +class TestAuthMultiFactor extends Command { + args: AuthSwitchArgs[]; + authFactor: number; + serverHello: unknown; + + constructor(args: AuthSwitchArgs[]) { + super(); + this.args = args; + this.authFactor = 0; + } + + start(_packet: unknown, connection: Connection) { + // @ts-expect-error: TODO: implement typings + const serverHelloPacket = new Packets.Handshake({ + // "required" properties + serverVersion: 'node.js rocks', + // the server should announce support for the + // "MULTI_FACTOR_AUTHENTICATION" capability + capabilityFlags: 0xdfffffff, + }); + this.serverHello = serverHelloPacket; + serverHelloPacket.setScrambleData(() => { + connection.writePacket(serverHelloPacket.toPacket(0)); + }); + return TestAuthMultiFactor.prototype.sendAuthSwitchRequest; + } + + sendAuthSwitchRequest(_packet: unknown, connection: Connection) { + // @ts-expect-error: TODO: implement typings + const asr = new Packets.AuthSwitchRequest(this.args[this.authFactor]); + connection.writePacket(asr.toPacket()); + return TestAuthMultiFactor.prototype.sendAuthNextFactor; + } + + sendAuthNextFactor(packet: unknown, connection: Connection): Function { + // @ts-expect-error: TODO: implement typings + const asr = Packets.AuthSwitchResponse.fromPacket(packet); + assert.deepStrictEqual( + asr.data.toString(), + this.args[this.authFactor].pluginName + ); + if (this.authFactor === 2) { + // send OK_Packet after the 3rd authentication factor + connection.writeOk(); + return TestAuthMultiFactor.prototype.dispatchCommands; + } + this.authFactor += 1; + // @ts-expect-error: TODO: implement typings + const anf = new Packets.AuthNextFactor(this.args[this.authFactor]); + // @ts-expect-error: TODO: implement typings + connection.writePacket(anf.toPacket(connection.serverConfig.encoding)); + return TestAuthMultiFactor.prototype.sendAuthNextFactor; + } + + dispatchCommands(_packet: unknown, connection: Connection) { + connection.end(); + return TestAuthMultiFactor.prototype.dispatchCommands; + } +} + +const server = mysql.createServer((conn: Connection) => { + // @ts-expect-error: TODO: implement typings + conn.serverConfig = {}; + // @ts-expect-error: TODO: implement typings + conn.serverConfig.encoding = 'cesu8'; + // @ts-expect-error: TODO: implement typings + conn.addCommand( + new TestAuthMultiFactor([ + { + // already covered by test-auth-switch + pluginName: 'auth_test_plugin1', + pluginData: Buffer.from('foo'), + }, + { + // 2nd factor auth plugin + pluginName: 'auth_test_plugin2', + pluginData: Buffer.from('bar'), + }, + { + // 3rd factor auth plugin + pluginName: 'auth_test_plugin3', + pluginData: Buffer.from('baz'), + }, + ]) + ); +}); + +const completed: string[] = []; + +portfinder.getPort((_err: Error | null, port: number) => { + server.listen(port); + const conn = mysql.createConnection({ + port: port, + password: 'secret1', + password2: 'secret2', + password3: 'secret3', + authPlugins: { + auth_test_plugin1() { + return () => { + const pluginName = 'auth_test_plugin1'; + completed.push(pluginName); + + return Buffer.from(pluginName); + }; + }, + auth_test_plugin2(options: { connection: Connection; command: string }) { + return () => { + if ( + options.connection.config.password !== + options.connection.config.password2 + ) { + return assert.fail('Incorrect authentication factor password.'); + } + + const pluginName = 'auth_test_plugin2'; + completed.push(pluginName); + + return Buffer.from(pluginName); + }; + }, + auth_test_plugin3(options: { connection: Connection; command: string }) { + return () => { + if ( + options.connection.config.password !== + options.connection.config.password3 + ) { + return assert.fail('Incorrect authentication factor password.'); + } + + const pluginName = 'auth_test_plugin3'; + completed.push(pluginName); + + return Buffer.from(pluginName); + }; + }, + }, + }); + + conn.on('connect', () => { + assert.deepStrictEqual(completed, [ + 'auth_test_plugin1', + 'auth_test_plugin2', + 'auth_test_plugin3', + ]); + + conn.end(); + // @ts-expect-error: TODO: implement typings + server.close(); + }); +}); diff --git a/test/esm/integration/test-auth-switch-plugin-async-error.test.mts b/test/esm/integration/test-auth-switch-plugin-async-error.test.mts new file mode 100644 index 0000000000..a2b4cf3f90 --- /dev/null +++ b/test/esm/integration/test-auth-switch-plugin-async-error.test.mts @@ -0,0 +1,102 @@ +// Copyright (c) 2021, Oracle and/or its affiliates. + +import type { AuthPlugin } from '../../../index.js'; +import assert from 'node:assert'; +import { Buffer } from 'node:buffer'; +import process from 'node:process'; +import portfinder from 'portfinder'; +import mysql from '../../../index.js'; +import Command from '../../../lib/commands/command.js'; +import Packets from '../../../lib/packets/index.js'; + +// The process is not terminated in Deno +if (typeof Deno !== 'undefined') process.exit(0); + +class TestAuthSwitchPluginError extends Command { + args: Record; + serverHello: unknown; + + constructor(args: Record) { + super(); + this.args = args; + } + + start(_packet: unknown, connection: unknown) { + // @ts-expect-error: TODO: implement typings + const serverHelloPacket = new Packets.Handshake({ + // "required" properties + protocolVersion: 10, + serverVersion: 'node.js rocks', + }); + this.serverHello = serverHelloPacket; + serverHelloPacket.setScrambleData(() => { + (connection as { writePacket: (p: unknown) => void }).writePacket( + serverHelloPacket.toPacket(0) + ); + }); + return TestAuthSwitchPluginError.prototype.sendAuthSwitchRequest; + } + + sendAuthSwitchRequest(_packet: unknown, connection: unknown) { + // @ts-expect-error: TODO: implement typings + const asr = new Packets.AuthSwitchRequest(this.args); + (connection as { writePacket: (p: unknown) => void }).writePacket( + asr.toPacket() + ); + return TestAuthSwitchPluginError.prototype.finish; + } + + finish(_packet: unknown, connection: unknown) { + (connection as { end: () => void }).end(); + return TestAuthSwitchPluginError.prototype.finish; + } +} + +const server = mysql.createServer((conn) => { + // @ts-expect-error: TODO: implement typings + conn.addCommand( + new TestAuthSwitchPluginError({ + pluginName: 'auth_test_plugin', + pluginData: Buffer.allocUnsafe(0), + }) + ); +}); + +let error: { code?: string; message?: string; fatal?: boolean } | undefined; +let uncaughtExceptions = 0; + +portfinder.getPort((_err, port) => { + server.listen(port); + const conn = mysql.createConnection({ + port: port, + authPlugins: { + auth_test_plugin: ((_plginMetadata) => + function (_pluginData) { + return Promise.reject(Error('boom')); + }) as AuthPlugin, + }, + }); + + conn.on('error', (err) => { + error = err as { code?: string; message?: string; fatal?: boolean }; + + conn.end(); + // @ts-expect-error: TODO: implement typings + server.close(); + }); +}); + +process.on('uncaughtException', (err) => { + // The plugin reports a fatal error + assert.equal(error?.code, 'AUTH_SWITCH_PLUGIN_ERROR'); + assert.equal(error?.message, 'boom'); + assert.equal(error?.fatal, true); + // The server must close the connection + assert.equal((err as { code?: string }).code, 'PROTOCOL_CONNECTION_LOST'); + + uncaughtExceptions += 1; +}); + +process.on('exit', () => { + assert.equal(uncaughtExceptions, 1); +}); diff --git a/test/esm/integration/test-auth-switch-plugin-error.test.mts b/test/esm/integration/test-auth-switch-plugin-error.test.mts new file mode 100644 index 0000000000..e8f2fdfac0 --- /dev/null +++ b/test/esm/integration/test-auth-switch-plugin-error.test.mts @@ -0,0 +1,100 @@ +// Copyright (c) 2021, Oracle and/or its affiliates. + +import assert from 'node:assert'; +import { Buffer } from 'node:buffer'; +import process from 'node:process'; +import portfinder from 'portfinder'; +import mysql from '../../../index.js'; +import Command from '../../../lib/commands/command.js'; +import Packets from '../../../lib/packets/index.js'; + +// The process is not terminated in Deno +if (typeof Deno !== 'undefined') process.exit(0); + +class TestAuthSwitchPluginError extends Command { + args: Record; + serverHello: unknown; + + constructor(args: Record) { + super(); + this.args = args; + } + + start(_packet: unknown, connection: unknown) { + // @ts-expect-error: TODO: implement typings + const serverHelloPacket = new Packets.Handshake({ + // "required" properties + protocolVersion: 10, + serverVersion: 'node.js rocks', + }); + this.serverHello = serverHelloPacket; + serverHelloPacket.setScrambleData(() => { + (connection as { writePacket: (p: unknown) => void }).writePacket( + serverHelloPacket.toPacket(0) + ); + }); + return TestAuthSwitchPluginError.prototype.sendAuthSwitchRequest; + } + + sendAuthSwitchRequest(_packet: unknown, connection: unknown) { + // @ts-expect-error: TODO: implement typings + const asr = new Packets.AuthSwitchRequest(this.args); + (connection as { writePacket: (p: unknown) => void }).writePacket( + asr.toPacket() + ); + return TestAuthSwitchPluginError.prototype.finish; + } + + finish(_packet: unknown, connection: unknown) { + (connection as { end: () => void }).end(); + return TestAuthSwitchPluginError.prototype.finish; + } +} + +const server = mysql.createServer((conn) => { + // @ts-expect-error: TODO: implement typings + conn.addCommand( + new TestAuthSwitchPluginError({ + pluginName: 'auth_test_plugin', + pluginData: Buffer.allocUnsafe(0), + }) + ); +}); + +let error: { code?: string; message?: string; fatal?: boolean } | undefined; +let uncaughtExceptions = 0; + +portfinder.getPort((_err, port) => { + server.listen(port); + const conn = mysql.createConnection({ + port: port, + authPlugins: { + auth_test_plugin: () => { + throw new Error('boom'); + }, + }, + }); + + conn.on('error', (err) => { + error = err as { code?: string; message?: string; fatal?: boolean }; + + conn.end(); + // @ts-expect-error: TODO: implement typings + server.close(); + }); +}); + +process.on('uncaughtException', (err) => { + // The plugin reports a fatal error + assert.equal(error?.code, 'AUTH_SWITCH_PLUGIN_ERROR'); + assert.equal(error?.message, 'boom'); + assert.equal(error?.fatal, true); + // The server must close the connection + assert.equal((err as { code?: string }).code, 'PROTOCOL_CONNECTION_LOST'); + + uncaughtExceptions += 1; +}); + +process.on('exit', () => { + assert.equal(uncaughtExceptions, 1); +}); diff --git a/test/esm/integration/test-auth-switch.test.mts b/test/esm/integration/test-auth-switch.test.mts new file mode 100644 index 0000000000..c650f3c9b8 --- /dev/null +++ b/test/esm/integration/test-auth-switch.test.mts @@ -0,0 +1,150 @@ +import type { Connection } from '../../../index.js'; +import { Buffer } from 'node:buffer'; +import process from 'node:process'; +import { assert } from 'poku'; +import portfinder from 'portfinder'; +import mysql from '../../../index.js'; +import Command from '../../../lib/commands/command.js'; +import Packets from '../../../lib/packets/index.js'; +import packageJson from '../../../package.json' with { type: 'json' }; + +type AuthSwitchArgs = { + pluginName: string; + pluginData: Buffer; +}; + +// The process is not terminated in Deno +if (typeof Deno !== 'undefined') process.exit(0); + +const connectAttributes = { foo: 'bar', baz: 'foo' }; + +const defaultConnectAttributes = { + _client_name: 'Node-MySQL-2', + _client_version: packageJson.version, +}; + +let count = 0; + +class TestAuthSwitchHandshake extends Command { + args: AuthSwitchArgs; + serverHello: unknown; + + constructor(args: AuthSwitchArgs) { + super(); + this.args = args; + } + + start(_packet: unknown, connection: Connection) { + // @ts-expect-error: TODO: implement typings + const serverHelloPacket = new Packets.Handshake({ + protocolVersion: 10, + serverVersion: 'node.js rocks', + connectionId: 1234, + statusFlags: 2, + characterSet: 8, + capabilityFlags: 0xffffff, + }); + this.serverHello = serverHelloPacket; + serverHelloPacket.setScrambleData(() => { + connection.writePacket(serverHelloPacket.toPacket(0)); + }); + return TestAuthSwitchHandshake.prototype.readClientReply; + } + + readClientReply(packet: unknown, connection: Connection) { + // @ts-expect-error: TODO: implement typings + const clientHelloReply = Packets.HandshakeResponse.fromPacket(packet); + assert.equal(clientHelloReply.user, 'test_user'); + assert.equal(clientHelloReply.database, 'test_database'); + assert.equal(clientHelloReply.authPluginName, 'mysql_native_password'); + assert.deepEqual(clientHelloReply.connectAttributes, { + ...connectAttributes, + ...defaultConnectAttributes, + }); + // @ts-expect-error: TODO: implement typings + const asr = new Packets.AuthSwitchRequest(this.args); + connection.writePacket(asr.toPacket()); + return TestAuthSwitchHandshake.prototype.readClientAuthSwitchResponse; + } + + readClientAuthSwitchResponse( + packet: unknown, + connection: Connection + ): (_packet: unknown, connection: Connection) => unknown { + // @ts-expect-error: TODO: implement typings + Packets.AuthSwitchResponse.fromPacket(packet); + count++; + if (count < 10) { + // @ts-expect-error: TODO: implement typings + const asrmd = new Packets.AuthSwitchRequestMoreData( + Buffer.from(`hahaha ${count}`) + ); + connection.writePacket(asrmd.toPacket()); + return TestAuthSwitchHandshake.prototype.readClientAuthSwitchResponse; + } + connection.writeOk(); + return TestAuthSwitchHandshake.prototype.dispatchCommands; + } + + dispatchCommands(_packet: unknown, connection: Connection) { + // Quit command here + // TODO: assert it's actually Quit + connection.end(); + return TestAuthSwitchHandshake.prototype.dispatchCommands; + } +} + +const server = mysql.createServer((conn: Connection) => { + // @ts-expect-error: TODO: implement typings + conn.serverConfig = {}; + // @ts-expect-error: TODO: implement typings + conn.serverConfig.encoding = 'cesu8'; + // @ts-expect-error: TODO: implement typings + conn.addCommand( + new TestAuthSwitchHandshake({ + pluginName: 'auth_test_plugin', + pluginData: Buffer.from('f{tU-{K@BhfHt/-4^Z,'), + }) + ); +}); + +portfinder.getPort((_err: Error | null, port: number) => { + const makeSwitchHandler = function () { + let count = 0; + return function ( + data: { pluginName: string; pluginData: Buffer }, + cb: (err: null, response: string) => void + ) { + if (count === 0) { + assert.equal(data.pluginName, 'auth_test_plugin'); + } else { + assert.equal(data.pluginData.toString(), `hahaha ${count}`); + } + + count++; + cb(null, `some data back${count}`); + }; + }; + + server.listen(port); + const conn = mysql.createConnection({ + user: 'test_user', + password: 'test', + database: 'test_database', + port: port, + authSwitchHandler: makeSwitchHandler(), + connectAttributes: connectAttributes, + }); + + conn.on( + 'connect', + (data: { serverVersion: string; connectionId: number }) => { + assert.equal(data.serverVersion, 'node.js rocks'); + assert.equal(data.connectionId, 1234); + + conn.end(); + // @ts-expect-error: TODO: implement typings + server.close(); + } + ); +}); diff --git a/test/esm/integration/test-handshake-unknown-packet-error.test.mts b/test/esm/integration/test-handshake-unknown-packet-error.test.mts new file mode 100644 index 0000000000..d753138af5 --- /dev/null +++ b/test/esm/integration/test-handshake-unknown-packet-error.test.mts @@ -0,0 +1,95 @@ +// Copyright (c) 2021, Oracle and/or its affiliates. + +import assert from 'node:assert'; +import { Buffer } from 'node:buffer'; +import process from 'node:process'; +import portfinder from 'portfinder'; +import mysql from '../../../index.js'; +import Command from '../../../lib/commands/command.js'; +import Packets from '../../../lib/packets/index.js'; +import Packet from '../../../lib/packets/packet.js'; + +// The process is not terminated in Deno +if (typeof Deno !== 'undefined') process.exit(0); + +class TestUnknownHandshakePacket extends Command { + args: Buffer; + // @ts-expect-error: TODO: implement typings + serverHello: InstanceType; + + constructor(args: Buffer) { + super(); + this.args = args; + } + + // @ts-expect-error: TODO: implement typings + start(_packet, connection) { + // @ts-expect-error: TODO: implement typings + const serverHelloPacket = new Packets.Handshake({ + // "required" properties + protocolVersion: 10, + serverVersion: 'node.js rocks', + }); + this.serverHello = serverHelloPacket; + serverHelloPacket.setScrambleData(() => { + connection.writePacket(serverHelloPacket.toPacket(0)); + }); + return TestUnknownHandshakePacket.prototype.writeUnexpectedPacket; + } + + // @ts-expect-error: TODO: implement typings + writeUnexpectedPacket(_packet, connection) { + const length = 6 + this.args.length; + const buffer = Buffer.allocUnsafe(length); + const up = new Packet(0, buffer, 0, length); + up.offset = 4; + up.writeInt8(0xfd); + up.writeBuffer(this.args); + connection.writePacket(up); + return TestUnknownHandshakePacket.prototype.finish; + } + + // @ts-expect-error: TODO: implement typings + finish(_packet, connection) { + connection.end(); + return TestUnknownHandshakePacket.prototype.finish; + } +} + +const server = mysql.createServer((conn) => { + // @ts-expect-error: TODO: implement typings + conn.addCommand(new TestUnknownHandshakePacket(Buffer.alloc(0))); +}); + +let error: { code?: string; message?: string; fatal?: boolean }; +let uncaughtExceptions = 0; + +portfinder.getPort((_err, port) => { + server.listen(port); + const conn = mysql.createConnection({ + port: port, + }); + + conn.on('error', (err) => { + error = err; + + conn.end(); + // @ts-expect-error: TODO: implement typings + server.close(); + }); +}); + +process.on('uncaughtException', (err: Error & { code?: string }) => { + // The plugin reports a fatal error + assert.equal(error.code, 'HANDSHAKE_UNKNOWN_ERROR'); + assert.equal(error.message, 'Unexpected packet during handshake phase'); + assert.equal(error.fatal, true); + // The server must close the connection + assert.equal(err.code, 'PROTOCOL_CONNECTION_LOST'); + + uncaughtExceptions += 1; +}); + +process.on('exit', () => { + assert.equal(uncaughtExceptions, 1); +}); diff --git a/test/esm/integration/test-multi-result-streaming.test.mts b/test/esm/integration/test-multi-result-streaming.test.mts new file mode 100644 index 0000000000..753170f101 --- /dev/null +++ b/test/esm/integration/test-multi-result-streaming.test.mts @@ -0,0 +1,52 @@ +import type { RowDataPacket } from '../../../index.js'; +import { assert } from 'poku'; +import { createConnection } from '../common.test.mjs'; + +const conn = createConnection({ multipleStatements: true }); +const captured1: RowDataPacket[] = []; +const captured2: RowDataPacket[] = []; +const sql1 = + 'select * from information_schema.columns order by table_schema, table_name, column_name limit 1;'; +const sql2 = + 'select * from information_schema.columns order by table_schema, table_name, ordinal_position limit 1;'; + +await conn.promise().query('set global max_allowed_packet=524288000'); + +const compare1 = await conn.promise().query(sql1); +const compare2 = await conn.promise().query(sql2); + +if (!compare1 || compare1.length < 1) { + assert.fail('no results for comparison 1'); +} +if (!compare2 || compare2.length < 1) { + assert.fail('no results for comparison 2'); +} + +const stream = conn.query(`${sql1}\n${sql2}`).stream(); +stream.on('result', (row: RowDataPacket, datasetIndex: number) => { + if (datasetIndex === 0) { + captured1.push(row); + } else { + captured2.push(row); + } +}); +// note: this is very important: +// after each result set is complete, +// the stream will emit "readable" and if we don't +// read then 'end' won't be emitted and the +// test will hang. +stream.on('readable', () => { + stream.read(); +}); + +await new Promise((resolve, reject) => { + stream.on('error', (e: Error) => reject(e)); + stream.on('end', () => resolve()); +}); + +assert.equal(captured1.length, 1); +assert.equal(captured2.length, 1); +assert.deepEqual(captured1[0], (compare1[0] as RowDataPacket[])[0]); +assert.deepEqual(captured2[0], (compare2[0] as RowDataPacket[])[0]); + +conn.end(); diff --git a/test/esm/integration/test-pool-connect-error.test.mts b/test/esm/integration/test-pool-connect-error.test.mts new file mode 100644 index 0000000000..0a9060fd68 --- /dev/null +++ b/test/esm/integration/test-pool-connect-error.test.mts @@ -0,0 +1,57 @@ +import process from 'node:process'; +import { assert } from 'poku'; +import portfinder from 'portfinder'; +import mysql from '../../../index.js'; + +// The process is not terminated in Deno +if (typeof Deno !== 'undefined') process.exit(0); + +const server = mysql.createServer((conn) => { + conn.serverHandshake({ + protocolVersion: 10, + serverVersion: '5.6.10', + connectionId: 1234, + statusFlags: 2, + characterSet: 8, + capabilityFlags: 0xffffff, + // @ts-expect-error: TODO: implement typings + authCallback: function (_params, cb) { + cb(null, { message: 'too many connections', code: 1040 }); + }, + }); +}); + +let err1: NodeJS.ErrnoException | undefined, + err2: NodeJS.ErrnoException | undefined; + +portfinder.getPort((_err, port) => { + server.listen(port); + const conn = mysql.createConnection({ + user: 'test_user', + password: 'test', + database: 'test_database', + port: port, + }); + conn.on('error', (err) => { + err1 = err; + }); + + const pool = mysql.createPool({ + user: 'test_user', + password: 'test', + database: 'test_database', + port: port, + }); + + pool.query('test sql', (err) => { + err2 = err ?? undefined; + pool.end(); + // @ts-expect-error: TODO: implement typings + server.close(); + }); +}); + +process.on('exit', () => { + assert.equal(err1?.errno, 1040); + assert.equal(err2?.errno, 1040); +}); diff --git a/test/esm/integration/test-pool-disconnect.test.mts b/test/esm/integration/test-pool-disconnect.test.mts new file mode 100644 index 0000000000..a17e2b6b2b --- /dev/null +++ b/test/esm/integration/test-pool-disconnect.test.mts @@ -0,0 +1,73 @@ +import type { + PoolConnection, + QueryError, + RowDataPacket, +} from '../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createConnection, createPool } from '../common.test.mjs'; + +// planetscale does not support KILL, skipping this test +// https://planetscale.com/docs/reference/mysql-compatibility +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test, planetscale does not support KILL'); + process.exit(0); +} + +const pool = createPool(); +const conn = createConnection({ multipleStatements: true }); +pool.config.connectionLimit = 5; + +const numSelectToPerform = 10; +const tids: number[] = []; +let numSelects = 0; +let killCount = 0; + +function kill() { + setTimeout(() => { + const id = tids.shift(); + if (typeof id !== 'undefined') { + // sleep required to give mysql time to close connection, + // and callback called after connection with id is really closed + conn.query('kill ?; select sleep(0.05)', [id], (err) => { + assert.ifError(err); + killCount++; + // TODO: this assertion needs to be fixed, after kill + // connection is removed from _allConnections but not at a point this callback is called + // + // assert.equal(pool._allConnections.length, tids.length); + }); + } else { + conn.end(); + pool.end(); + } + }, 5); +} + +pool.on('connection', (conn: PoolConnection) => { + tids.push(conn.threadId); + conn.on('error', () => { + setTimeout(kill, 5); + }); +}); + +for (let i = 0; i < numSelectToPerform; i++) { + pool.query( + 'select 1 as value', + (err: QueryError | null, rows: RowDataPacket[]) => { + numSelects++; + assert.ifError(err); + assert.equal(rows[0].value, 1); + + // after all queries complete start killing connections + if (numSelects === numSelectToPerform) { + kill(); + } + } + ); +} + +process.on('exit', () => { + assert.equal(numSelects, numSelectToPerform); + assert.equal(killCount, pool.config.connectionLimit); +}); diff --git a/test/esm/integration/test-pool-end.test.mts b/test/esm/integration/test-pool-end.test.mts new file mode 100644 index 0000000000..d9ce84ff5f --- /dev/null +++ b/test/esm/integration/test-pool-end.test.mts @@ -0,0 +1,25 @@ +import { assert } from 'poku'; +import { createPool } from '../common.test.mjs'; + +const pool = createPool(); + +pool.getConnection((err, conn) => { + assert.ifError(err); + + // @ts-expect-error: internal access + assert(pool._allConnections.length === 1); + // @ts-expect-error: internal access + assert(pool._freeConnections.length === 0); + + // emit the end event, so the connection gets removed from the pool + // @ts-expect-error: internal access + conn.stream.emit('end'); + + // @ts-expect-error: internal access + assert(pool._allConnections.length === 0); + // @ts-expect-error: internal access + assert(pool._freeConnections.length === 0); + + // As the connection has not really ended we need to do this ourselves + conn.destroy(); +}); diff --git a/test/esm/integration/test-pool-release-idle-connection-replicate.test.mts b/test/esm/integration/test-pool-release-idle-connection-replicate.test.mts new file mode 100644 index 0000000000..2a2b33243b --- /dev/null +++ b/test/esm/integration/test-pool-release-idle-connection-replicate.test.mts @@ -0,0 +1,81 @@ +import type { PoolConnection } from '../../../index.js'; +import { assert } from 'poku'; +import { createPool } from '../common.test.mjs'; + +/** + * This test case tests the scenario where released connections are not + * being destroyed after the idle timeout has passed, to do this we setup + * a pool with a connection limit of 3, and a max idle of 2, and an idle + * timeout of 1 second. We then create 3 connections, and release them + * after 1 second, we then check that the pool has 0 connections, and 0 + * free connections. + * + * @see https://github.com/sidorares/node-mysql2/issues/3020 + */ + +/** + * This test case + */ +const pool = createPool({ + connectionLimit: 3, + maxIdle: 2, + idleTimeout: 1000, +}); + +/** + * Create the first connection and ensure it's in the pool as expected + */ +pool.getConnection( + (err1: NodeJS.ErrnoException | null, connection1: PoolConnection) => { + assert.ifError(err1); + assert.ok(connection1); + + /** + * Create the second connection and ensure it's in the pool as expected + */ + pool.getConnection( + (err2: NodeJS.ErrnoException | null, connection2: PoolConnection) => { + assert.ifError(err2); + assert.ok(connection2); + + /** + * Create the third connection and ensure it's in the pool as expected + */ + pool.getConnection( + (err3: NodeJS.ErrnoException | null, connection3: PoolConnection) => { + assert.ifError(err3); + assert.ok(connection3); + + /** + * Release all the connections + */ + connection1.release(); + connection2.release(); + connection3.release(); + + /** + * After the idle timeout has passed, check that all items in the in the pool + * that have been released are destroyed as expected. + */ + setTimeout(() => { + assert( + // @ts-expect-error: internal access + pool._allConnections.length === 0, + // @ts-expect-error: internal access + `Expected all connections to be closed, but found ${pool._allConnections.length}` + ); + assert( + // @ts-expect-error: internal access + pool._freeConnections.length === 0, + // @ts-expect-error: internal access + `Expected all free connections to be closed, but found ${pool._freeConnections.length}` + ); + + pool.end(); + }, 5000); + } + ); + } + ); + } +); diff --git a/test/esm/integration/test-pool-release-idle-connection-timeout.test.mts b/test/esm/integration/test-pool-release-idle-connection-timeout.test.mts new file mode 100644 index 0000000000..cae8841057 --- /dev/null +++ b/test/esm/integration/test-pool-release-idle-connection-timeout.test.mts @@ -0,0 +1,62 @@ +import type { PoolConnection } from '../../../index.js'; +import { assert } from 'poku'; +import { createPool } from '../common.test.mjs'; + +const pool = createPool({ + connectionLimit: 3, // 5 connections + maxIdle: 1, // 1 idle connection + idleTimeout: 1000, // remove idle connections after 1 second +}); + +pool.getConnection( + (err1: NodeJS.ErrnoException | null, connection1: PoolConnection) => { + assert.ifError(err1); + assert.ok(connection1); + pool.getConnection( + (err2: NodeJS.ErrnoException | null, connection2: PoolConnection) => { + assert.ifError(err2); + assert.ok(connection2); + assert.notStrictEqual(connection1, connection2); + pool.getConnection( + (err3: NodeJS.ErrnoException | null, connection3: PoolConnection) => { + assert.ifError(err3); + assert.ok(connection3); + assert.notStrictEqual(connection1, connection3); + assert.notStrictEqual(connection2, connection3); + connection1.release(); + connection2.release(); + connection3.release(); + // @ts-expect-error: internal access + assert(pool._allConnections.length === 3); + // @ts-expect-error: internal access + assert(pool._freeConnections.length === 3); + // after two seconds, the above 3 connection should have been destroyed + setTimeout(() => { + // @ts-expect-error: internal access + assert(pool._allConnections.length === 0); + // @ts-expect-error: internal access + assert(pool._freeConnections.length === 0); + // Creating a new connection should create a fresh one + pool.getConnection( + ( + err4: NodeJS.ErrnoException | null, + connection4: PoolConnection + ) => { + assert.ifError(err4); + assert.ok(connection4); + // @ts-expect-error: internal access + assert(pool._allConnections.length === 1); + // @ts-expect-error: internal access + assert(pool._freeConnections.length === 0); + connection4.release(); + connection4.destroy(); + pool.end(); + } + ); + }, 2000); + } + ); + } + ); + } +); diff --git a/test/esm/integration/test-pool-release-idle-connection.test.mts b/test/esm/integration/test-pool-release-idle-connection.test.mts new file mode 100644 index 0000000000..5a567c239c --- /dev/null +++ b/test/esm/integration/test-pool-release-idle-connection.test.mts @@ -0,0 +1,63 @@ +import type { PoolConnection } from '../../../index.js'; +import { assert } from 'poku'; +import { createPool } from '../common.test.mjs'; + +const pool = createPool({ + connectionLimit: 5, // 5 connections + maxIdle: 1, // 1 idle connection + idleTimeout: 5000, // 5 seconds +}); + +pool.getConnection( + (err1: NodeJS.ErrnoException | null, connection1: PoolConnection) => { + assert.ifError(err1); + assert.ok(connection1); + pool.getConnection( + (err2: NodeJS.ErrnoException | null, connection2: PoolConnection) => { + assert.ifError(err2); + assert.ok(connection2); + assert.notStrictEqual(connection1, connection2); + pool.getConnection( + (err3: NodeJS.ErrnoException | null, connection3: PoolConnection) => { + assert.ifError(err3); + assert.ok(connection3); + assert.notStrictEqual(connection1, connection3); + assert.notStrictEqual(connection2, connection3); + connection1.release(); + connection2.release(); + connection3.release(); + // @ts-expect-error: internal access + assert(pool._allConnections.length === 3); + // @ts-expect-error: internal access + assert(pool._freeConnections.length === 3); + setTimeout(() => { + // @ts-expect-error: internal access + assert(pool._allConnections.length === 1); + // @ts-expect-error: internal access + assert(pool._freeConnections.length === 1); + pool.getConnection( + ( + err4: NodeJS.ErrnoException | null, + connection4: PoolConnection + ) => { + assert.ifError(err4); + assert.ok(connection4); + assert.strictEqual(connection3, connection4); + // @ts-expect-error: internal access + assert(pool._allConnections.length === 1); + // @ts-expect-error: internal access + assert(pool._freeConnections.length === 0); + connection4.release(); + connection4.destroy(); + pool.end(); + } + ); + // Setting the time to a lower value than idleTimeout will ensure that the connection is not considered idle + // during our assertions + }, 4000); + } + ); + } + ); + } +); diff --git a/test/esm/integration/test-pool-release.test.mts b/test/esm/integration/test-pool-release.test.mts new file mode 100644 index 0000000000..717c962791 --- /dev/null +++ b/test/esm/integration/test-pool-release.test.mts @@ -0,0 +1,41 @@ +import { assert } from 'poku'; +import { createPool } from '../common.test.mjs'; + +const pool = createPool({ + idleTimeout: 15000, +}); + +pool.query('test sql', () => { + pool.query('test sql', [], () => { + pool.query('test sql', [], () => { + pool.query('test sql', [], () => { + pool.query('test sql', () => { + pool.query('test sql').on('error', () => { + pool.query('test sql', () => { + pool.execute('test sql', () => { + pool.execute('test sql', () => { + pool.execute('test sql', [], () => { + pool.execute('test sql', () => { + pool.execute('test sql', () => { + // TODO change order events are fires so that connection is released before callback + // that way this number will be more deterministic + // @ts-expect-error: internal access + assert(pool._allConnections.length < 3); + // on some setups with small CLIENT_INTERACTION_TIMEOUT value connection might be closed by the time we get here, hence "one or zero" + // @ts-expect-error: internal access + assert(pool._freeConnections.length <= 1); + // @ts-expect-error: internal access + assert(pool._connectionQueue.length === 0); + pool.end(); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); +}); diff --git a/test/esm/integration/test-pool.test.mts b/test/esm/integration/test-pool.test.mts index a175ddc95a..dc3ef9a75c 100644 --- a/test/esm/integration/test-pool.test.mts +++ b/test/esm/integration/test-pool.test.mts @@ -1,4 +1,4 @@ -import { assert, it, describe } from 'poku'; +import { assert, describe, it } from 'poku'; import mysql from '../../../index.js'; const poolConfig = {}; // config: { connectionConfig: {} }; diff --git a/test/esm/integration/test-rows-as-array.test.mts b/test/esm/integration/test-rows-as-array.test.mts new file mode 100644 index 0000000000..112d02a538 --- /dev/null +++ b/test/esm/integration/test-rows-as-array.test.mts @@ -0,0 +1,67 @@ +import type { QueryError, RowDataPacket } from '../../../index.js'; +import { assert } from 'poku'; +import { createConnection } from '../common.test.mjs'; + +// enabled in initial config, disable in some tets +const c = createConnection({ rowsAsArray: true }); +c.query('select 1+1 as a', (err: QueryError | null, rows: RowDataPacket[]) => { + assert.ifError(err); + assert.equal(rows[0][0], 2); +}); + +c.query( + { sql: 'select 1+2 as a', rowsAsArray: false }, + (err: QueryError | null, rows: RowDataPacket[]) => { + assert.ifError(err); + assert.equal(rows[0].a, 3); + } +); + +c.execute( + 'select 1+1 as a', + (err: QueryError | null, rows: RowDataPacket[]) => { + assert.ifError(err); + assert.equal(rows[0][0], 2); + } +); + +c.execute( + { sql: 'select 1+2 as a', rowsAsArray: false }, + (err: QueryError | null, rows: RowDataPacket[]) => { + assert.ifError(err); + assert.equal(rows[0].a, 3); + c.end(); + } +); + +// disabled in initial config, enable in some tets +const c1 = createConnection({ rowsAsArray: false }); +c1.query('select 1+1 as a', (err: QueryError | null, rows: RowDataPacket[]) => { + assert.ifError(err); + assert.equal(rows[0].a, 2); +}); + +c1.query( + { sql: 'select 1+2 as a', rowsAsArray: true }, + (err: QueryError | null, rows: RowDataPacket[]) => { + assert.ifError(err); + assert.equal(rows[0][0], 3); + } +); + +c1.execute( + 'select 1+1 as a', + (err: QueryError | null, rows: RowDataPacket[]) => { + assert.ifError(err); + assert.equal(rows[0].a, 2); + } +); + +c1.execute( + { sql: 'select 1+2 as a', rowsAsArray: true }, + (err: QueryError | null, rows: RowDataPacket[]) => { + assert.ifError(err); + assert.equal(rows[0][0], 3); + c1.end(); + } +); diff --git a/test/esm/integration/test-server-close.test.mts b/test/esm/integration/test-server-close.test.mts new file mode 100644 index 0000000000..9811fc9697 --- /dev/null +++ b/test/esm/integration/test-server-close.test.mts @@ -0,0 +1,54 @@ +// Copyright (c) 2021, Oracle and/or its affiliates. + +import type { QueryError } from '../../../index.js'; +import assert from 'node:assert'; +import process from 'node:process'; +import errors from '../../../lib/constants/errors.js'; +import { createConnection } from '../common.test.mjs'; + +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test for planetscale'); + process.exit(0); +} + +// Uncaught AssertionError: Connection lost: The server closed the connection. == The client was disconnected by the server because of inactivity. See wait_timeout and interactive_timeout for configuring this behavior. +if (typeof Deno !== 'undefined') process.exit(0); + +const connection = createConnection(); + +const customWaitTimeout = 1; // seconds + +let error: QueryError; + +connection.on('error', (err) => { + error = err as QueryError; + + // @ts-expect-error: TODO: implement typings + connection.close(); +}); + +connection.query(`set wait_timeout=${customWaitTimeout}`, () => { + setTimeout(() => {}, customWaitTimeout * 1000 * 2); +}); + +process.on('uncaughtException', (err) => { + // The ERR Packet is only sent by MySQL server 8.0.24 or higher, so we + // need to account for the fact it is not sent by older server versions. + if ((err as NodeJS.ErrnoException).code !== 'ERR_ASSERTION') { + throw err; + } + + assert.equal( + error.message, + 'Connection lost: The server closed the connection.' + ); + assert.equal(error.code, 'PROTOCOL_CONNECTION_LOST'); +}); + +process.on('exit', () => { + assert.equal( + error.message, + 'The client was disconnected by the server because of inactivity. See wait_timeout and interactive_timeout for configuring this behavior.' + ); + assert.equal(error.code, errors.ER_CLIENT_INTERACTION_TIMEOUT); +}); diff --git a/test/esm/regressions/2052.test.mts b/test/esm/regressions/2052.test.mts index 6871350998..ddb103bfe7 100644 --- a/test/esm/regressions/2052.test.mts +++ b/test/esm/regressions/2052.test.mts @@ -1,8 +1,8 @@ -import { assert, describe, it } from 'poku'; +import type { PrepareStatementInfo, QueryError } from '../../../index.js'; import { Buffer } from 'node:buffer'; -import packets from '../../../lib/packets/index.js'; +import { assert, describe, it } from 'poku'; import PrepareCommand from '../../../lib/commands/prepare.js'; -import type { QueryError, PrepareStatementInfo } from '../../../index.js'; +import packets from '../../../lib/packets/index.js'; import { createConnection, getMysqlVersion } from '../common.test.mjs'; await describe(async () => { @@ -20,7 +20,7 @@ await describe(async () => { config: { charsetNumber: 33, }, - writePacket: (packet: any) => { + writePacket: (packet: InstanceType) => { // client -> server COM_PREPARE packet.writeHeader(1); assert.equal( diff --git a/test/esm/tsconfig.json b/test/esm/tsconfig.json index d6b5d6b384..d3a4294cee 100644 --- a/test/esm/tsconfig.json +++ b/test/esm/tsconfig.json @@ -1,5 +1,5 @@ { - "include": ["**/*.mts"], + "include": ["**/*.mts", "../globals.d.ts"], "compilerOptions": { "target": "ESNext", "lib": ["ESNext"], @@ -11,13 +11,17 @@ "allowJs": true, "strict": true, "alwaysStrict": true, - "strictFunctionTypes": false, + "strictFunctionTypes": true, "strictNullChecks": true, - "noImplicitAny": false, + "resolveJsonModule": true, + "noImplicitAny": true, "noImplicitThis": false, "noUnusedLocals": true, "noUnusedParameters": true, "allowUnreachableCode": false, - "allowUnusedLabels": false + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "allowUnusedLabels": false, + "skipLibCheck": true } } diff --git a/test/esm/unit/check-extensions.test.mts b/test/esm/unit/check-extensions.test.mts index 1ab3760af0..34cd161b98 100644 --- a/test/esm/unit/check-extensions.test.mts +++ b/test/esm/unit/check-extensions.test.mts @@ -1,5 +1,5 @@ import { EOL } from 'node:os'; -import { listFiles, it, assert, describe } from 'poku'; +import { assert, describe, it, listFiles } from 'poku'; await describe('Check for invalid file types found in restricted directories', async () => { const invalidFiles: string[] = []; diff --git a/test/esm/unit/commands/test-query.test.mts b/test/esm/unit/commands/test-query.test.mts new file mode 100644 index 0000000000..0f00ec1f39 --- /dev/null +++ b/test/esm/unit/commands/test-query.test.mts @@ -0,0 +1,18 @@ +import { assert } from 'poku'; +import Query from '../../../../lib/commands/query.js'; + +const testError = new Error('something happened'); +const testQuery = new Query({}, (err: Error | null, res: unknown) => { + assert.equal(err, testError); + assert.equal(res, null); +}); + +testQuery._rowParser = new (class FailingRowParser { + next() { + throw testError; + } +})(); + +testQuery.row({ + isEOF: () => false, +}); diff --git a/test/esm/unit/commands/test-quit.test.mts b/test/esm/unit/commands/test-quit.test.mts new file mode 100644 index 0000000000..5ccf78f8fb --- /dev/null +++ b/test/esm/unit/commands/test-quit.test.mts @@ -0,0 +1,7 @@ +import { assert } from 'poku'; +import Quit from '../../../../lib/commands/quit.js'; + +const testCallback = (err: Error) => console.info(err.message); +const testQuit = new Quit(testCallback); + +assert.strictEqual(testQuit.onResult, testCallback); diff --git a/test/esm/unit/connection/test-connection-state.test.mts b/test/esm/unit/connection/test-connection-state.test.mts new file mode 100644 index 0000000000..8dfc17910d --- /dev/null +++ b/test/esm/unit/connection/test-connection-state.test.mts @@ -0,0 +1,156 @@ +import EventEmitter from 'node:events'; +import { assert, test } from 'poku'; +import BaseConnection from '../../../../lib/base/connection.js'; +import ConnectionConfig from '../../../../lib/connection_config.js'; + +// Helper to create a mock connection without actually connecting +function createMockConnection() { + const config = new ConnectionConfig({ + host: 'localhost', + user: 'test', + password: 'test', + database: 'test', + connectTimeout: 0, + }); + + // Create a minimal mock stream + const mockStream = Object.assign(new EventEmitter(), { + write: () => true, + end: () => {}, + destroy() { + this.destroyed = true; + }, + destroyed: false, + setKeepAlive: () => {}, + setNoDelay: () => {}, + }); + + config.stream = mockStream; + config.isServer = true; // Prevent handshake command + + return new BaseConnection({ config }); +} + +test('should return disconnected state when no stream exists', () => { + const conn = createMockConnection(); + conn.stream = null; + assert.strictEqual( + conn.state, + 'disconnected', + 'State should be "disconnected" when stream is null' + ); +}); + +test('should return protocol_handshake state when stream exists but handshake not complete', () => { + const conn = createMockConnection(); + assert.strictEqual( + conn.state, + 'protocol_handshake', + 'State should be "protocol_handshake" when stream exists but handshake not complete' + ); +}); + +test('should return error state when fatal error occurs', () => { + const conn = createMockConnection(); + conn._fatalError = new Error('Fatal error'); + assert.strictEqual( + conn.state, + 'error', + 'State should be "error" when _fatalError is set' + ); +}); + +test('should return error state when protocol error occurs', () => { + const conn = createMockConnection(); + conn._protocolError = new Error('Protocol error'); + assert.strictEqual( + conn.state, + 'error', + 'State should be "error" when _protocolError is set' + ); +}); + +test('should return disconnected state when closing', () => { + const conn = createMockConnection(); + conn._closing = true; + assert.strictEqual( + conn.state, + 'disconnected', + 'State should be "disconnected" when _closing is true' + ); +}); + +test('should return disconnected state when stream is destroyed', () => { + const conn = createMockConnection(); + conn.stream.destroy(); + assert.strictEqual( + conn.state, + 'disconnected', + 'State should be "disconnected" when stream is destroyed' + ); +}); + +test('should return connected state when handshake is complete but not authorized', () => { + const conn = createMockConnection(); + conn._handshakePacket = { connectionId: 123 }; + assert.strictEqual( + conn.state, + 'connected', + 'State should be "connected" when handshake is complete but not authorized' + ); +}); + +test('should return authenticated state when authorized', () => { + const conn = createMockConnection(); + conn.authorized = true; + assert.strictEqual( + conn.state, + 'authenticated', + 'State should be "authenticated" when authorized is true' + ); +}); + +test('should return error state even when authorized and closing (error has highest priority)', () => { + const conn = createMockConnection(); + conn.authorized = true; + conn._closing = true; + conn._fatalError = new Error('Fatal error'); + assert.strictEqual( + conn.state, + 'error', + 'State should be "error" even when authorized and closing (error has highest priority)' + ); +}); + +test('should return disconnected state even when authorized (closing has higher priority)', () => { + const conn = createMockConnection(); + conn.authorized = true; + conn._closing = true; + assert.strictEqual( + conn.state, + 'disconnected', + 'State should be "disconnected" even when authorized (closing has higher priority)' + ); +}); + +test('should return error state when protocol error is set, regardless of authorization', () => { + const conn = createMockConnection(); + conn.authorized = true; + conn._protocolError = new Error('Protocol error'); + assert.strictEqual( + conn.state, + 'error', + 'State should be "error" when _protocolError is set, regardless of authorization' + ); +}); + +test('should return authenticated state when both handshake complete and authorized (authenticated has priority)', () => { + const conn = createMockConnection(); + conn._handshakePacket = { connectionId: 123 }; + conn.authorized = true; + assert.strictEqual( + conn.state, + 'authenticated', + 'State should be "authenticated" when both handshake complete and authorized (authenticated has priority)' + ); +}); diff --git a/test/esm/unit/connection/test-connection_config.test.mts b/test/esm/unit/connection/test-connection_config.test.mts new file mode 100644 index 0000000000..d8313f4ca5 --- /dev/null +++ b/test/esm/unit/connection/test-connection_config.test.mts @@ -0,0 +1,76 @@ +import { assert } from 'poku'; +import ConnectionConfig from '../../../../lib/connection_config.js'; +import SSLProfiles from '../../../../lib/constants/ssl_profiles.js'; + +const expectedMessage = "SSL profile must be an object, instead it's a boolean"; + +assert.throws( + () => + new ConnectionConfig({ + ssl: true, + }), + (err: unknown) => err instanceof TypeError && err.message === expectedMessage, + 'Error, the constructor accepts a boolean without throwing the right exception' +); + +assert.doesNotThrow( + () => + new ConnectionConfig({ + ssl: {}, + }), + 'Error, the constructor accepts an object but throws an exception' +); + +assert.doesNotThrow(() => { + const sslProfile = Object.keys(SSLProfiles)[0]; + new ConnectionConfig({ + ssl: sslProfile, + }); +}, 'Error, the constructor accepts a string but throws an exception'); + +assert.doesNotThrow(() => { + new ConnectionConfig({ + flags: '-FOUND_ROWS', + }); +}, 'Error, the constructor threw an exception due to a flags string'); + +assert.doesNotThrow(() => { + new ConnectionConfig({ + flags: ['-FOUND_ROWS'], + }); +}, 'Error, the constructor threw an exception due to a flags array'); + +assert.strictEqual( + ConnectionConfig.parseUrl( + String.raw`fml://test:pass!%40%24%25%5E%26*()word%3A@www.example.com/database` + ).password, + 'pass!@$%^&*()word:' +); + +assert.strictEqual( + ConnectionConfig.parseUrl( + String.raw`fml://user%40test.com:pass!%40%24%25%5E%26*()word%3A@www.example.com/database` + ).user, + 'user@test.com' +); + +assert.strictEqual( + ConnectionConfig.parseUrl( + String.raw`fml://test:pass@wordA@fe80%3A3438%3A7667%3A5c77%3Ace27%2518/database` + ).host, + 'fe80:3438:7667:5c77:ce27%18' +); + +assert.strictEqual( + ConnectionConfig.parseUrl( + String.raw`fml://test:pass@wordA@www.example.com/database` + ).host, + 'www.example.com' +); + +assert.strictEqual( + ConnectionConfig.parseUrl( + String.raw`fml://test:pass@wordA@www.example.com/database%24` + ).database, + 'database$' +); diff --git a/test/esm/unit/packets/test-column-definition.test.mts b/test/esm/unit/packets/test-column-definition.test.mts new file mode 100644 index 0000000000..64d64eda46 --- /dev/null +++ b/test/esm/unit/packets/test-column-definition.test.mts @@ -0,0 +1,81 @@ +import { assert } from 'poku'; +import ColumnDefinition from '../../../../lib/packets/column_definition.js'; + +const sequenceId = 5; + +// simple +let packet = ColumnDefinition.toPacket( + { + catalog: 'def', + schema: 'some_db', + name: 'some_col', + orgName: 'some_col', + table: 'some_tbl', + orgTable: 'some_tbl', + + characterSet: 0x21, + columnLength: 500, + flags: 32896, + columnType: 0x8, + decimals: 1, + }, + sequenceId +); +assert.equal( + packet.buffer.toString('hex', 4), + '0364656607736f6d655f646208736f6d655f74626c08736f6d655f74626c08736f6d655f636f6c08736f6d655f636f6c0c2100f4010000088080010000' +); + +// Russian +packet = ColumnDefinition.toPacket( + { + catalog: 'def', + schema: 's_погоди', + name: 'n_погоди', + orgName: 'on_погоди', + table: 't_погоди', + orgTable: 'ot_погоди', + characterSet: 0x21, + columnLength: 500, + flags: 32896, + columnType: 0x8, + decimals: 1, + }, + sequenceId +); +assert.equal( + packet.buffer.toString('hex', 4), + '036465660e735fd0bfd0bed0b3d0bed0b4d0b80e745fd0bfd0bed0b3d0bed0b4d0b80f6f745fd0bfd0bed0b3d0bed0b4d0b80e6e5fd0bfd0bed0b3d0bed0b4d0b80f6f6e5fd0bfd0bed0b3d0bed0b4d0b80c2100f4010000088080010000' +); + +// Spec (from example: https://dev.mysql.com/doc/internals/en/protocoltext-resultset.html) +const inputColDef = { + catalog: 'def', + schema: '', + name: '@@version_comment', + orgName: '', + table: '', + orgTable: '', + + characterSet: 0x08, // latin1_swedish_ci + columnLength: 0x1c, + flags: 0, + columnType: 0xfd, + type: 0xfd, + encoding: 'latin1', + decimals: 0x1f, +}; +packet = ColumnDefinition.toPacket(inputColDef, sequenceId); +assert.equal( + packet.buffer.toString('hex', 4), + '0364656600000011404076657273696f6e5f636f6d6d656e74000c08001c000000fd00001f0000' +); + +packet.offset = 4; +const colDef = new ColumnDefinition(packet, 'utf8'); +// inspect omits the "colulumnType" property because type is an alias for it +// but ColumnDefinition.toPacket reads type from "columnType" +// TODO: think how to make this more consistent +const inspect = { columnType: 253, ...colDef.inspect() }; +assert.deepEqual(inspect, inputColDef); +assert.equal(colDef.db, inputColDef.schema); diff --git a/test/esm/unit/packets/test-datetime.test.mts b/test/esm/unit/packets/test-datetime.test.mts new file mode 100644 index 0000000000..a03c9efc1f --- /dev/null +++ b/test/esm/unit/packets/test-datetime.test.mts @@ -0,0 +1,28 @@ +import { Buffer } from 'node:buffer'; +import { assert } from 'poku'; +import Packets from '../../../../lib/packets/index.js'; + +let buf = Buffer.from('0a000004000007dd070116010203', 'hex'); + +let packet = new Packets.Packet(4, buf, 0, buf.length); +packet.readInt16(); // unused +let d = packet.readDateTime('Z'); +if (d === null) throw new Error('expected d to be non-null'); +assert.equal(+d, 1358816523000); + +buf = Buffer.from( + '18000006000004666f6f310be00702090f01095d7f06000462617231', + 'hex' +); +packet = new Packets.Packet(6, buf, 0, buf.length); + +packet.readInt16(); // ignore +const s = packet.readLengthCodedString('cesu8'); +assert.equal(s, 'foo1'); +d = packet.readDateTime('Z'); +if (d === null) throw new Error('expected d to be non-null'); +assert.equal(+d, 1455030069425); + +const s1 = packet.readLengthCodedString('cesu8'); +assert.equal(s1, 'bar1'); +assert.equal(packet.offset, packet.end); diff --git a/test/esm/unit/packets/test-ok-autoinc.test.mts b/test/esm/unit/packets/test-ok-autoinc.test.mts new file mode 100644 index 0000000000..e2d30a0d9a --- /dev/null +++ b/test/esm/unit/packets/test-ok-autoinc.test.mts @@ -0,0 +1,14 @@ +import { assert } from 'poku'; +import Packets from '../../../../lib/packets/index.js'; + +const packet = Packets.OK.toPacket({ affectedRows: 0, insertId: 1 }); + +// 5 bytes for an OK packet, plus one byte to store affectedRows plus one byte to store the insertId +assert.equal( + packet.length(), + 11, + `${ + 'OK packets with 0 affectedRows and a minimal insertId should be ' + + '11 bytes long, got ' + }${packet.length()} byte(s)` +); diff --git a/test/esm/unit/packets/test-ok-sessiontrack.test.mts b/test/esm/unit/packets/test-ok-sessiontrack.test.mts new file mode 100644 index 0000000000..a23035f459 --- /dev/null +++ b/test/esm/unit/packets/test-ok-sessiontrack.test.mts @@ -0,0 +1,38 @@ +import { Buffer } from 'node:buffer'; +import { assert } from 'poku'; +import clientConstants from '../../../../lib/constants/client.js'; +import Packet from '../../../../lib/packets/packet.js'; +import ResultSetHeader from '../../../../lib/packets/resultset_header.js'; + +const mockConnection = { + config: {}, + serverEncoding: 'utf8', + _handshakePacket: { + capabilityFlags: + clientConstants.PROTOCOL_41 + clientConstants.SESSION_TRACK, + }, +}; + +const mkpacket = (str: string) => { + const buf = Buffer.from(str.split(/[ \n]+/).join(''), 'hex'); + return new Packet(0, buf, 0, buf.length); +}; + +// regression examples from https://github.com/sidorares/node-mysql2/issues/989 + +assert.doesNotThrow(() => { + const packet = mkpacket( + `1b 00 00 01 + 00 + 01 fe 65 96 fc 02 00 00 00 00 03 40 00 00 00 0a 14 08 fe 60 63 9b 05 00 00 00 + ` + ); + new ResultSetHeader(packet, mockConnection); +}); + +assert.doesNotThrow(() => { + const packet = mkpacket( + `13 00 00 01 00 01 00 02 40 00 00 00 0a 14 08 fe 18 25 e7 06 00 00 00` + ); + new ResultSetHeader(packet, mockConnection); +}); diff --git a/test/esm/unit/packets/test-text-row.test.mts b/test/esm/unit/packets/test-text-row.test.mts new file mode 100644 index 0000000000..e627c6c293 --- /dev/null +++ b/test/esm/unit/packets/test-text-row.test.mts @@ -0,0 +1,25 @@ +import { assert } from 'poku'; +import TextRow from '../../../../lib/packets/text_row.js'; + +// simple +let packet = TextRow.toPacket(['Hello', 'World'], 'cesu8'); +assert.equal(packet.buffer.toString('hex', 4), '0548656c6c6f05576f726c64'); + +// Russian (unicode) +packet = TextRow.toPacket(['Ну,', 'погоди!'], 'cesu8'); +assert.equal( + packet.buffer.toString('hex', 4), + '05d09dd1832c0dd0bfd0bed0b3d0bed0b4d0b821' +); + +// Long > 256 byte +packet = TextRow.toPacket( + [ + 'Пушкин родился 26 мая (6 июня) 1799 г. в Москве. В метрической книге церкви Богоявления в Елохове (сейчас на её месте находится Богоявленский собор в Елохове) на дату 8 июня 1799 г.', + ], + 'cesu8' +); +assert.equal( + packet.buffer.toString('hex', 4), + 'fc3801d09fd183d188d0bad0b8d0bd20d180d0bed0b4d0b8d0bbd181d18f20323620d0bcd0b0d18f20283620d0b8d18ed0bdd18f29203137393920d0b32e20d0b220d09cd0bed181d0bad0b2d0b52e20d09220d0bcd0b5d182d180d0b8d187d0b5d181d0bad0bed0b920d0bad0bdd0b8d0b3d0b520d186d0b5d180d0bad0b2d0b820d091d0bed0b3d0bed18fd0b2d0bbd0b5d0bdd0b8d18f20d0b220d095d0bbd0bed185d0bed0b2d0b52028d181d0b5d0b9d187d0b0d18120d0bdd0b020d0b5d19120d0bcd0b5d181d182d0b520d0bdd0b0d185d0bed0b4d0b8d182d181d18f20d091d0bed0b3d0bed18fd0b2d0bbd0b5d0bdd181d0bad0b8d0b920d181d0bed0b1d0bed18020d0b220d095d0bbd0bed185d0bed0b2d0b52920d0bdd0b020d0b4d0b0d182d183203820d0b8d18ed0bdd18f203137393920d0b32e' +); diff --git a/test/esm/unit/packets/test-time.test.mts b/test/esm/unit/packets/test-time.test.mts new file mode 100644 index 0000000000..19c3085a2b --- /dev/null +++ b/test/esm/unit/packets/test-time.test.mts @@ -0,0 +1,20 @@ +import { Buffer } from 'node:buffer'; +import { assert } from 'poku'; +import Packets from '../../../../lib/packets/index.js'; + +[ + ['01:23:45', '0b000004000008000000000001172d'], // CONVERT('01:23:45', TIME) + ['01:23:45.123456', '0f00000400000c000000000001172d40e20100'], // DATE_ADD(CONVERT('01:23:45', TIME), INTERVAL 0.123456 SECOND) + ['-01:23:44.876544', '0f00000400000c010000000001172c00600d00'], // DATE_ADD(CONVERT('-01:23:45', TIME), INTERVAL 0.123456 SECOND) + ['-81:23:44.876544', '0f00000400000c010300000009172c00600d00'], // DATE_ADD(CONVERT('-81:23:45', TIME), INTERVAL 0.123456 SECOND) + ['81:23:45', '0b000004000008000300000009172d'], // CONVERT('81:23:45', TIME) + ['123:23:45.123456', '0f00000400000c000500000003172d40e20100'], // DATE_ADD(CONVERT('123:23:45', TIME), INTERVAL 0.123456 SECOND) + ['-121:23:45', '0b000004000008010500000001172d'], // CONVERT('-121:23:45', TIME) + ['-01:23:44.88', '0f00000400000c010000000001172c806d0d00'], //DATE_ADD(CONVERT('-01:23:45', TIME), INTERVAL 0.12 SECOND) +].forEach(([expected, buffer]) => { + const buf = Buffer.from(buffer as string, 'hex'); + const packet = new Packets.Packet(4, buf, 0, buf.length); + packet.readInt16(); // unused + const d = packet.readTimeString(false); + assert.equal(d, expected); +}); diff --git a/test/esm/unit/parsers/big-numbers-strings-binary-sanitization.test.mts b/test/esm/unit/parsers/big-numbers-strings-binary-sanitization.test.mts index 30f5186718..b8af9afa67 100644 --- a/test/esm/unit/parsers/big-numbers-strings-binary-sanitization.test.mts +++ b/test/esm/unit/parsers/big-numbers-strings-binary-sanitization.test.mts @@ -1,6 +1,9 @@ -import { describe, it, assert } from 'poku'; +import type { RowDataPacket } from '../../../../index.js'; +import { assert, describe, it } from 'poku'; import { createConnection } from '../../common.test.mjs'; +type TotalRow = RowDataPacket & { total: number | string }; + await describe('Binary Parser: bigNumberStrings Sanitization', async () => { const connection = createConnection().promise(); @@ -33,7 +36,10 @@ await describe('Binary Parser: bigNumberStrings Sanitization', async () => { for (const [options, expectedType, label] of cases) { await it(label, async () => { - const [results] = await connection.execute({ sql, ...options }); + const [results] = await connection.execute({ + sql, + ...options, + }); assert.strictEqual(typeof results[0].total, expectedType, label); }); diff --git a/test/esm/unit/parsers/big-numbers-strings-text-sanitization.test.mts b/test/esm/unit/parsers/big-numbers-strings-text-sanitization.test.mts index 5ad83d615f..dd450531a5 100644 --- a/test/esm/unit/parsers/big-numbers-strings-text-sanitization.test.mts +++ b/test/esm/unit/parsers/big-numbers-strings-text-sanitization.test.mts @@ -1,6 +1,9 @@ -import { describe, it, assert } from 'poku'; +import type { RowDataPacket } from '../../../../index.js'; +import { assert, describe, it } from 'poku'; import { createConnection } from '../../common.test.mjs'; +type TotalRow = RowDataPacket & { total: number | string }; + await describe('Text Parser: bigNumberStrings Sanitization', async () => { const connection = createConnection().promise(); @@ -33,7 +36,7 @@ await describe('Text Parser: bigNumberStrings Sanitization', async () => { for (const [options, expectedType, label] of cases) { await it(label, async () => { - const [results] = await connection.query({ sql, ...options }); + const [results] = await connection.query({ sql, ...options }); assert.strictEqual(typeof results[0].total, expectedType, label); }); diff --git a/test/esm/unit/parsers/cache-key-serialization.test.mts b/test/esm/unit/parsers/cache-key-serialization.test.mts index ba51dda6ef..d0f05fadc7 100644 --- a/test/esm/unit/parsers/cache-key-serialization.test.mts +++ b/test/esm/unit/parsers/cache-key-serialization.test.mts @@ -1,7 +1,46 @@ -import { describe, it, assert } from 'poku'; import type { TypeCastField, TypeCastNext } from '../../../../index.js'; +import { assert, describe, it } from 'poku'; import { _keyFromFields } from '../../../../lib/parsers/parser_cache.js'; +interface CacheKeyTestData { + type: string | undefined; + fields: { + name: string | undefined; + columnType: string | undefined; + length: undefined; + schema: string | undefined; + table: string | undefined; + flags: string | undefined; + characterSet: string | undefined; + }[]; + options: { + nestTables: boolean | string | undefined; + rowsAsArray: boolean | number | undefined; + supportBigNumbers: boolean | string | undefined; + bigNumberStrings: + | boolean + | unknown[] + | ((_: unknown, next: () => void) => void) + | undefined; + typeCast: + | boolean + | ((field: TypeCastField, next: TypeCastNext) => void) + | undefined; + timezone: string | undefined; + decimalNumbers: boolean | { a: null } | undefined; + dateStrings: + | boolean + | string + | ((_: unknown, next: () => void) => void) + | undefined; + }; + config: { + supportBigNumbers: boolean | undefined; + bigNumberStrings: boolean | undefined; + timezone: boolean | string | undefined; + }; +} + describe('Cache Key Serialization', () => { // Invalid const test1 = { @@ -275,7 +314,7 @@ describe('Cache Key Serialization', () => { nestTables: true, rowsAsArray: 2, supportBigNumbers: 'yes', - bigNumberStrings: [] as any[], + bigNumberStrings: [] as unknown[], typeCast: true, timezone: 'local', decimalNumbers: { @@ -309,13 +348,13 @@ describe('Cache Key Serialization', () => { rowsAsArray: false, supportBigNumbers: false, // Expected: true - bigNumberStrings: (_: any, next: any) => next(), + bigNumberStrings: (_: unknown, next: () => void) => next(), // Expected: "function" typeCast: (_: TypeCastField, next: TypeCastNext) => next(), timezone: 'local', decimalNumbers: false, // Expected: null - dateStrings: (_: any, next: any) => next(), + dateStrings: (_: unknown, next: () => void) => next(), }, config: { supportBigNumbers: undefined, @@ -364,7 +403,7 @@ describe('Cache Key Serialization', () => { }, }; - const keyFrom = (t: any): string => + const keyFrom = (t: CacheKeyTestData): string => _keyFromFields(t.type, t.fields, t.options, t.config); it(() => { @@ -491,8 +530,12 @@ describe('Cache Key Serialization', () => { const stringify = JSON.stringify; // Overwriting the native `JSON.stringify` - JSON.stringify = (value: any, replacer?: any, space: any = 8) => - stringify(value, replacer, space); + // @ts-expect-error: overwriting JSON.stringify for testing + JSON.stringify = ( + value: unknown, + replacer?: Parameters[1], + space: string | number = 8 + ) => stringify(value, replacer, space); const result1 = keyFrom(test1); diff --git a/test/esm/unit/parsers/ensure-safe-binary-fields.test.mts b/test/esm/unit/parsers/ensure-safe-binary-fields.test.mts index 54c1726ef4..63670ac303 100644 --- a/test/esm/unit/parsers/ensure-safe-binary-fields.test.mts +++ b/test/esm/unit/parsers/ensure-safe-binary-fields.test.mts @@ -1,6 +1,6 @@ -import { describe, it, assert } from 'poku'; -import getBinaryParser from '../../../../lib/parsers/binary_parser.js'; +import { assert, describe, it } from 'poku'; import { privateObjectProps } from '../../../../lib/helpers.js'; +import getBinaryParser from '../../../../lib/parsers/binary_parser.js'; describe('Binary Parser: Block Native Object Props', () => { const blockedFields: { name: string; table: string }[][] = Array.from( diff --git a/test/esm/unit/parsers/ensure-safe-text-fields.test.mts b/test/esm/unit/parsers/ensure-safe-text-fields.test.mts index 02c742fb5a..9b0646bedf 100644 --- a/test/esm/unit/parsers/ensure-safe-text-fields.test.mts +++ b/test/esm/unit/parsers/ensure-safe-text-fields.test.mts @@ -1,6 +1,6 @@ -import { describe, it, assert } from 'poku'; -import TextRowParser from '../../../../lib/parsers/text_parser.js'; +import { assert, describe, it } from 'poku'; import { privateObjectProps } from '../../../../lib/helpers.js'; +import TextRowParser from '../../../../lib/parsers/text_parser.js'; describe('Text Parser: Block Native Object Props', () => { const blockedFields: { name: string; table: string }[][] = Array.from( diff --git a/test/esm/unit/parsers/support-big-numbers-binary-sanitization.test.mts b/test/esm/unit/parsers/support-big-numbers-binary-sanitization.test.mts index 30dd062122..b1153c5cd5 100644 --- a/test/esm/unit/parsers/support-big-numbers-binary-sanitization.test.mts +++ b/test/esm/unit/parsers/support-big-numbers-binary-sanitization.test.mts @@ -1,6 +1,9 @@ -import { describe, it, assert } from 'poku'; +import type { RowDataPacket } from '../../../../index.js'; +import { assert, describe, it } from 'poku'; import { createConnection } from '../../common.test.mjs'; +type TotalRow = RowDataPacket & { total: number | string }; + await describe('Binary Parser: supportBigNumbers Sanitization', async () => { const connection = createConnection().promise(); @@ -25,7 +28,7 @@ await describe('Binary Parser: supportBigNumbers Sanitization', async () => { for (const [supportBigNumbers, expectedType, label] of cases) { await it(label, async () => { // @ts-expect-error: TODO: implement typings - const [results] = await connection.execute({ + const [results] = await connection.execute({ sql, supportBigNumbers, }); diff --git a/test/esm/unit/parsers/support-big-numbers-text-sanitization.test.mts b/test/esm/unit/parsers/support-big-numbers-text-sanitization.test.mts index b46185f369..a4f06e1aa3 100644 --- a/test/esm/unit/parsers/support-big-numbers-text-sanitization.test.mts +++ b/test/esm/unit/parsers/support-big-numbers-text-sanitization.test.mts @@ -1,6 +1,9 @@ -import { describe, it, assert } from 'poku'; +import type { RowDataPacket } from '../../../../index.js'; +import { assert, describe, it } from 'poku'; import { createConnection } from '../../common.test.mjs'; +type TotalRow = RowDataPacket & { total: number | string }; + await describe('Text Parser: supportBigNumbers Sanitization', async () => { const connection = createConnection().promise(); @@ -25,7 +28,7 @@ await describe('Text Parser: supportBigNumbers Sanitization', async () => { for (const [supportBigNumbers, expectedType, label] of cases) { await it(label, async () => { // @ts-expect-error: TODO: implement typings - const [results] = await connection.query({ + const [results] = await connection.query({ sql, supportBigNumbers, }); diff --git a/test/esm/unit/parsers/test-text-parser.test.mts b/test/esm/unit/parsers/test-text-parser.test.mts new file mode 100644 index 0000000000..04ebca9428 --- /dev/null +++ b/test/esm/unit/parsers/test-text-parser.test.mts @@ -0,0 +1,49 @@ +import type { + RowDataPacket, + TypeCastField, + TypeCastNext, +} from '../../../../index.js'; +import { assert } from 'poku'; +import { createConnection } from '../../common.test.mjs'; + +const typeCastWrapper = function ( + ...args: [encoding?: BufferEncoding | string | undefined] +) { + return function (field: TypeCastField, next: TypeCastNext) { + if (field.type === 'JSON') { + return JSON.parse(field.string(...args) ?? ''); + } + + return next(); + }; +}; + +const connection = createConnection(); +connection.query('CREATE TEMPORARY TABLE t (i JSON)'); +connection.query('INSERT INTO t values(\'{ "test": "😀" }\')'); + +// JSON without encoding options - should result in unexpected behaviors +connection.query( + { + sql: 'SELECT * FROM t', + typeCast: typeCastWrapper(), + }, + (err, rows) => { + assert.ifError(err); + assert.notEqual(rows[0].i.test, '😀'); + } +); + +// JSON with encoding explicitly set to utf8 +connection.query( + { + sql: 'SELECT * FROM t', + typeCast: typeCastWrapper('utf8'), + }, + (err, rows) => { + assert.ifError(err); + assert.equal(rows[0].i.test, '😀'); + } +); + +connection.end(); diff --git a/test/esm/unit/parsers/timezone-binary-sanitization.test.mts b/test/esm/unit/parsers/timezone-binary-sanitization.test.mts index b03ed86dba..520de8985c 100644 --- a/test/esm/unit/parsers/timezone-binary-sanitization.test.mts +++ b/test/esm/unit/parsers/timezone-binary-sanitization.test.mts @@ -1,5 +1,5 @@ import process from 'node:process'; -import { describe, it, assert } from 'poku'; +import { assert, describe, it } from 'poku'; import { createConnection } from '../../common.test.mjs'; await describe('Binary Parser: timezone Sanitization', async () => { diff --git a/test/esm/unit/parsers/timezone-text-sanitization.test.mts b/test/esm/unit/parsers/timezone-text-sanitization.test.mts index 6e515b9356..d752b2334d 100644 --- a/test/esm/unit/parsers/timezone-text-sanitization.test.mts +++ b/test/esm/unit/parsers/timezone-text-sanitization.test.mts @@ -1,5 +1,5 @@ import process from 'node:process'; -import { describe, it, assert } from 'poku'; +import { assert, describe, it } from 'poku'; import { createConnection } from '../../common.test.mjs'; await describe('Text Parser: timezone Sanitization', async () => { diff --git a/test/esm/unit/pool-cluster/test-connection-error-remove.test.mts b/test/esm/unit/pool-cluster/test-connection-error-remove.test.mts new file mode 100644 index 0000000000..6e06505c72 --- /dev/null +++ b/test/esm/unit/pool-cluster/test-connection-error-remove.test.mts @@ -0,0 +1,88 @@ +import process, { exit } from 'node:process'; +import { assert } from 'poku'; +import portfinder from 'portfinder'; +import mysql from '../../../../index.js'; +import { createPoolCluster } from '../../common.test.mjs'; + +// The process is not terminated in Deno +if (typeof Deno !== 'undefined') process.exit(0); + +// TODO: config poolCluster to work with MYSQL_CONNECTION_URL run +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test for planetscale'); + process.exit(0); +} + +if (process.platform === 'win32') { + console.log('This test is known to fail on windows. FIXME: investi=gate why'); + exit(0); +} + +const cluster = createPoolCluster({ + removeNodeErrorCount: 1, +}); + +let connCount = 0; + +// @ts-expect-error: TODO: implement typings +const server1 = mysql.createServer(); +// @ts-expect-error: TODO: implement typings +const server2 = mysql.createServer(); + +console.log('test pool cluster error remove'); + +portfinder.getPort((_err, port) => { + cluster.add('SLAVE1', { port: port + 0 }); + cluster.add('SLAVE2', { port: port + 1 }); + + // @ts-expect-error: TODO: implement typings + server1.listen(port + 0, (err) => { + assert.ifError(err); + + // @ts-expect-error: TODO: implement typings + server2.listen(port + 1, (err) => { + assert.ifError(err); + + const pool = cluster.of('*', 'ORDER'); + let removedNodeId: string | number; + + cluster.on('remove', (nodeId) => { + removedNodeId = nodeId; + }); + + pool.getConnection((err, connection) => { + assert.ifError(err); + + assert.equal(connCount, 2); + // @ts-expect-error: internal access + assert.equal(connection._clusterId, 'SLAVE2'); + assert.equal(removedNodeId, 'SLAVE1'); + // @ts-expect-error: internal access + assert.deepEqual(cluster._serviceableNodeIds, ['SLAVE2']); + console.log('done'); + + connection.release(); + + cluster.end((err) => { + assert.ifError(err); + // throw error if no exit() + exit(); + // server1.close(); + // server2.close(); + }); + }); + }); + }); + + server1.on('connection', (conn) => { + connCount += 1; + conn.close(); + }); + + server2.on('connection', (conn) => { + connCount += 1; + conn.serverHandshake({ + serverVersion: 'node.js rocks', + }); + }); +}); diff --git a/test/esm/unit/pool-cluster/test-connection-order.test.mts b/test/esm/unit/pool-cluster/test-connection-order.test.mts new file mode 100644 index 0000000000..e0dcdc3944 --- /dev/null +++ b/test/esm/unit/pool-cluster/test-connection-order.test.mts @@ -0,0 +1,48 @@ +import process from 'node:process'; +import { assert } from 'poku'; +import { createPoolCluster, getConfig } from '../../common.test.mjs'; + +// TODO: config poolCluster to work with MYSQL_CONNECTION_URL run +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test for planetscale'); + process.exit(0); +} + +const cluster = createPoolCluster(); + +const order: string[] = []; + +const poolConfig = getConfig(); +cluster.add('SLAVE1', poolConfig); +cluster.add('SLAVE2', poolConfig); + +const done = function () { + assert.deepEqual(order, ['SLAVE1', 'SLAVE1', 'SLAVE1', 'SLAVE1', 'SLAVE1']); + cluster.end(); + console.log('done'); +}; + +const pool = cluster.of('SLAVE*', 'ORDER'); + +console.log('test pool cluster connection ORDER'); + +let count = 0; + +function getConnection(i: number) { + pool.getConnection((err, conn) => { + assert.ifError(err); + // @ts-expect-error: internal access + order[i] = conn._clusterId; + conn.release(); + + count += 1; + + if (count <= 4) { + getConnection(count); + } else { + done(); + } + }); +} + +getConnection(0); diff --git a/test/esm/unit/pool-cluster/test-connection-retry.test.mts b/test/esm/unit/pool-cluster/test-connection-retry.test.mts new file mode 100644 index 0000000000..0bf96af214 --- /dev/null +++ b/test/esm/unit/pool-cluster/test-connection-retry.test.mts @@ -0,0 +1,71 @@ +import process, { exit } from 'node:process'; +import { assert } from 'poku'; +import portfinder from 'portfinder'; +import mysql from '../../../../index.js'; +import { createPoolCluster } from '../../common.test.mjs'; + +// The process is not terminated in Deno +if (typeof Deno !== 'undefined') process.exit(0); + +// TODO: config poolCluster to work with MYSQL_CONNECTION_URL run +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test for planetscale'); + process.exit(0); +} + +if (process.platform === 'win32') { + console.log('This test is known to fail on windows. FIXME: investi=gate why'); + exit(0); +} + +const cluster = createPoolCluster({ + canRetry: true, + removeNodeErrorCount: 5, +}); + +let connCount = 0; + +// @ts-expect-error: TODO: implement typings +const server = mysql.createServer(); + +console.log('test pool cluster retry'); + +portfinder.getPort((_err, port) => { + cluster.add('MASTER', { port }); + + // @ts-expect-error: TODO: implement typings + server.listen(port + 0, (err) => { + assert.ifError(err); + + cluster.getConnection('MASTER', (err, connection) => { + assert.ifError(err); + assert.equal(connCount, 2); + // @ts-expect-error: internal access + assert.equal(connection._clusterId, 'MASTER'); + + connection.release(); + + cluster.end((err) => { + assert.ifError(err); + // @ts-expect-error: TODO: implement typings + server.close(); + }); + }); + }); + + server.on('connection', (conn) => { + connCount += 1; + + if (connCount < 2) { + conn.close(); + } else { + conn.serverHandshake({ + serverVersion: 'node.js rocks', + }); + conn.on('error', () => { + // server side of the connection + // ignore disconnects + }); + } + }); +}); diff --git a/test/esm/unit/pool-cluster/test-connection-rr.test.mts b/test/esm/unit/pool-cluster/test-connection-rr.test.mts new file mode 100644 index 0000000000..b384502095 --- /dev/null +++ b/test/esm/unit/pool-cluster/test-connection-rr.test.mts @@ -0,0 +1,48 @@ +import process from 'node:process'; +import { assert } from 'poku'; +import { createPoolCluster, getConfig } from '../../common.test.mjs'; + +// TODO: config poolCluster to work with MYSQL_CONNECTION_URL run +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test for planetscale'); + process.exit(0); +} + +const cluster = createPoolCluster(); + +const order: string[] = []; + +const poolConfig = getConfig(); +cluster.add('SLAVE1', poolConfig); +cluster.add('SLAVE2', poolConfig); + +const done = function () { + assert.deepEqual(order, ['SLAVE1', 'SLAVE2', 'SLAVE1', 'SLAVE2', 'SLAVE1']); + cluster.end(); + console.log('done'); +}; + +const pool = cluster.of('SLAVE*', 'RR'); + +console.log('test pool cluster connection RR'); + +let count = 0; + +function getConnection(i: number) { + pool.getConnection((err, conn) => { + assert.ifError(err); + // @ts-expect-error: internal access + order[i] = conn._clusterId; + conn.release(); + + count += 1; + + if (count <= 4) { + getConnection(count); + } else { + done(); + } + }); +} + +getConnection(0); diff --git a/test/esm/unit/pool-cluster/test-query.test.mts b/test/esm/unit/pool-cluster/test-query.test.mts new file mode 100644 index 0000000000..8f180d4a99 --- /dev/null +++ b/test/esm/unit/pool-cluster/test-query.test.mts @@ -0,0 +1,32 @@ +import type { RowDataPacket } from '../../../../index.js'; +import process from 'node:process'; +import { assert } from 'poku'; +import { createPoolCluster, getConfig } from '../../common.test.mjs'; + +// TODO: config poolCluster to work with MYSQL_CONNECTION_URL run +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test for planetscale'); + process.exit(0); +} + +const cluster = createPoolCluster(); +const poolConfig = getConfig(); + +cluster.add('MASTER', poolConfig); +cluster.add('SLAVE1', poolConfig); +cluster.add('SLAVE2', poolConfig); + +const connection = cluster.of('*'); + +console.log('test pool cluster connection query'); + +connection.query('SELECT 1', (err, rows) => { + assert.ifError(err); + assert.equal(rows.length, 1); + assert.equal(rows[0]['1'], 1); + // @ts-expect-error: internal access + assert.deepEqual(cluster._serviceableNodeIds, ['MASTER', 'SLAVE1', 'SLAVE2']); + + cluster.end(); + console.log('done'); +}); diff --git a/test/esm/unit/pool-cluster/test-remove-by-name.test.mts b/test/esm/unit/pool-cluster/test-remove-by-name.test.mts new file mode 100644 index 0000000000..e865d3aaa1 --- /dev/null +++ b/test/esm/unit/pool-cluster/test-remove-by-name.test.mts @@ -0,0 +1,79 @@ +import process from 'node:process'; +import { assert } from 'poku'; +import portfinder from 'portfinder'; +import mysql from '../../../../index.js'; +import { createPoolCluster } from '../../common.test.mjs'; + +// TODO: config poolCluster to work with MYSQL_CONNECTION_URL run +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test for planetscale'); + process.exit(0); +} + +if (process.platform === 'win32') { + console.log('This test is known to fail on windows. FIXME: investi=gate why'); + process.exit(0); +} + +// The process is not terminated in Deno +if (typeof Deno !== 'undefined') process.exit(0); + +const cluster = createPoolCluster(); +// @ts-expect-error: TODO: implement typings +const server = mysql.createServer(); + +console.log('test pool cluster remove by name'); + +portfinder.getPort((_err, port) => { + cluster.add('SLAVE1', { port }); + cluster.add('SLAVE2', { port }); + + // @ts-expect-error: TODO: implement typings + server.listen(port + 0, (err) => { + assert.ifError(err); + + const pool = cluster.of('SLAVE*', 'ORDER'); + + pool.getConnection((err, conn) => { + assert.ifError(err); + // @ts-expect-error: internal access + assert.strictEqual(conn._clusterId, 'SLAVE1'); + + conn.release(); + cluster.remove('SLAVE1'); + + pool.getConnection((err, conn) => { + assert.ifError(err); + // @ts-expect-error: internal access + assert.strictEqual(conn._clusterId, 'SLAVE2'); + + conn.release(); + cluster.remove('SLAVE2'); + + pool.getConnection((err) => { + assert.ok(err); + assert.equal(err?.code, 'POOL_NOEXIST'); + + cluster.remove('SLAVE1'); + cluster.remove('SLAVE2'); + + cluster.end((err) => { + assert.ifError(err); + // @ts-expect-error: TODO: implement typings + server.close(); + }); + }); + }); + }); + }); + + server.on('connection', (conn) => { + conn.serverHandshake({ + serverVersion: 'node.js rocks', + }); + conn.on('error', () => { + // server side of the connection + // ignore disconnects + }); + }); +}); diff --git a/test/esm/unit/pool-cluster/test-remove-by-pattern.test.mts b/test/esm/unit/pool-cluster/test-remove-by-pattern.test.mts new file mode 100644 index 0000000000..ac811f2b10 --- /dev/null +++ b/test/esm/unit/pool-cluster/test-remove-by-pattern.test.mts @@ -0,0 +1,70 @@ +import process from 'node:process'; +import { assert } from 'poku'; +import portfinder from 'portfinder'; +import mysql from '../../../../index.js'; +import { createPoolCluster } from '../../common.test.mjs'; + +// TODO: config poolCluster to work with MYSQL_CONNECTION_URL run +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test for planetscale'); + process.exit(0); +} + +if (process.platform === 'win32') { + console.log('This test is known to fail on windows. FIXME: investi=gate why'); + process.exit(0); +} + +// The process is not terminated in Deno +if (typeof Deno !== 'undefined') process.exit(0); + +const cluster = createPoolCluster(); +// @ts-expect-error: TODO: implement typings +const server = mysql.createServer(); + +console.log('test pool cluster remove by pattern'); + +portfinder.getPort((_err, port) => { + cluster.add('SLAVE1', { port }); + cluster.add('SLAVE2', { port }); + + // @ts-expect-error: TODO: implement typings + server.listen(port + 0, (err) => { + assert.ifError(err); + + const pool = cluster.of('SLAVE*', 'ORDER'); + + pool.getConnection((err, conn) => { + assert.ifError(err); + // @ts-expect-error: internal access + assert.strictEqual(conn._clusterId, 'SLAVE1'); + + conn.release(); + cluster.remove('SLAVE*'); + + pool.getConnection((err) => { + assert.ok(err); + assert.equal(err?.code, 'POOL_NOEXIST'); + + cluster.remove('SLAVE*'); + cluster.remove('SLAVE2'); + + cluster.end((err) => { + assert.ifError(err); + // @ts-expect-error: TODO: implement typings + server.close(); + }); + }); + }); + }); + + server.on('connection', (conn) => { + conn.serverHandshake({ + serverVersion: 'node.js rocks', + }); + conn.on('error', () => { + // server side of the connection + // ignore disconnects + }); + }); +}); diff --git a/test/esm/unit/pool-cluster/test-restore-events.test.mts b/test/esm/unit/pool-cluster/test-restore-events.test.mts new file mode 100644 index 0000000000..c44b58a4cc --- /dev/null +++ b/test/esm/unit/pool-cluster/test-restore-events.test.mts @@ -0,0 +1,100 @@ +import process from 'node:process'; +import { assert } from 'poku'; +import portfinder from 'portfinder'; +import mysql from '../../../../index.js'; +import { createPoolCluster } from '../../common.test.mjs'; + +// TODO: config poolCluster to work with MYSQL_CONNECTION_URL run +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test for planetscale'); + process.exit(0); +} + +if (process.platform === 'win32') { + console.log('This test is known to fail on windows. FIXME: investi=gate why'); + process.exit(0); +} + +// The process is not terminated in Deno +if (typeof Deno !== 'undefined') process.exit(0); + +const cluster = createPoolCluster({ + canRetry: true, + removeNodeErrorCount: 2, + restoreNodeTimeout: 100, +}); + +let connCount = 0; +let offline = true; +let offlineEvents = 0; +let onlineEvents = 0; + +// @ts-expect-error: TODO: implement typings +const server = mysql.createServer(); + +console.log('test pool cluster restore events'); + +portfinder.getPort((_err, port) => { + cluster.add('MASTER', { port }); + + // @ts-expect-error: TODO: implement typings + server.listen(port + 0, (err) => { + assert.ifError(err); + + cluster.on('offline', (id) => { + assert.equal(++offlineEvents, 1); + assert.equal(id, 'MASTER'); + assert.equal(connCount, 2); + + cluster.getConnection('MASTER', (err) => { + assert.ok(err); + assert.equal(err?.code, 'POOL_NONEONLINE'); + + offline = false; + }); + + setTimeout(() => { + cluster.getConnection('MASTER', (err, conn) => { + assert.ifError(err); + conn.release(); + }); + }, 200); + }); + + cluster.on('online', (id) => { + assert.equal(++onlineEvents, 1); + assert.equal(id, 'MASTER'); + assert.equal(connCount, 3); + + cluster.end((err) => { + assert.ifError(err); + // @ts-expect-error: TODO: implement typings + server.close(); + }); + }); + + cluster.getConnection('MASTER', (err) => { + assert.ok(err); + assert.equal(err?.code, 'PROTOCOL_CONNECTION_LOST'); + // @ts-expect-error: TODO: implement typings + assert.equal(err?.fatal, true); + assert.equal(connCount, 2); + }); + }); + + server.on('connection', (conn) => { + connCount += 1; + + if (offline) { + conn.close(); + } else { + conn.serverHandshake({ + serverVersion: 'node.js rocks', + }); + conn.on('error', () => { + // server side of the connection + // ignore disconnects + }); + } + }); +}); diff --git a/test/esm/unit/pool-cluster/test-restore.test.mts b/test/esm/unit/pool-cluster/test-restore.test.mts new file mode 100644 index 0000000000..dfa15712e9 --- /dev/null +++ b/test/esm/unit/pool-cluster/test-restore.test.mts @@ -0,0 +1,89 @@ +import process from 'node:process'; +import { assert } from 'poku'; +import portfinder from 'portfinder'; +import mysql from '../../../../index.js'; +import { createPoolCluster } from '../../common.test.mjs'; + +// TODO: config poolCluster to work with MYSQL_CONNECTION_URL run +if (`${process.env.MYSQL_CONNECTION_URL}`.includes('pscale_pw_')) { + console.log('skipping test for planetscale'); + process.exit(0); +} + +if (process.platform === 'win32') { + console.log('This test is known to fail on windows. FIXME: investi=gate why'); + process.exit(0); +} + +// The process is not terminated in Deno +if (typeof Deno !== 'undefined') process.exit(0); + +const cluster = createPoolCluster({ + canRetry: true, + removeNodeErrorCount: 2, + restoreNodeTimeout: 100, +}); + +let connCount = 0; +let offline = true; + +// @ts-expect-error: TODO: implement typings +const server = mysql.createServer(); + +console.log('test pool cluster restore'); + +portfinder.getPort((_err, port) => { + cluster.add('MASTER', { port }); + + // @ts-expect-error: TODO: implement typings + server.listen(port + 0, (err) => { + assert.ifError(err); + + cluster.getConnection('MASTER', (err) => { + assert.ok(err); + assert.equal(err?.code, 'PROTOCOL_CONNECTION_LOST'); + // @ts-expect-error: TODO: implement typings + assert.equal(err?.fatal, true); + assert.equal(connCount, 2); + + cluster.getConnection('MASTER', (err) => { + assert.ok(err); + assert.equal(err?.code, 'POOL_NONEONLINE'); + + // @ts-expect-error: internal access + cluster._nodes.MASTER.errorCount = 3; + + offline = false; + }); + + setTimeout(() => { + cluster.getConnection('MASTER', (err, conn) => { + assert.ifError(err); + conn.release(); + + cluster.end((err) => { + assert.ifError(err); + // @ts-expect-error: TODO: implement typings + server.close(); + }); + }); + }, 200); + }); + }); + + server.on('connection', (conn) => { + connCount += 1; + + if (offline) { + conn.close(); + } else { + conn.serverHandshake({ + serverVersion: 'node.js rocks', + }); + conn.on('error', () => { + // server side of the connection + // ignore disconnects + }); + } + }); +}); diff --git a/test/esm/unit/protocol/SqlString.test.mts b/test/esm/unit/protocol/SqlString.test.mts index 9a7bef5fb2..3643c9b1a1 100644 --- a/test/esm/unit/protocol/SqlString.test.mts +++ b/test/esm/unit/protocol/SqlString.test.mts @@ -1,5 +1,5 @@ -import { assert, it, describe } from 'poku'; import { Buffer } from 'node:buffer'; +import { assert, describe, it } from 'poku'; import { SqlString } from '../../common.test.mjs'; describe('SqlString.escapeId tests', () => { diff --git a/test/esm/unit/test-packet-parser.test.mts b/test/esm/unit/test-packet-parser.test.mts new file mode 100644 index 0000000000..8489b12437 --- /dev/null +++ b/test/esm/unit/test-packet-parser.test.mts @@ -0,0 +1,150 @@ +import { Buffer } from 'node:buffer'; +import { assert } from 'poku'; +import PacketParser from '../../../lib/packet_parser.js'; +import Packet from '../../../lib/packets/packet.js'; + +type PacketInstance = InstanceType; + +let pp: InstanceType; +let packets: PacketInstance[] = []; +const handler = function (p: PacketInstance) { + packets.push(p); +}; +function reset() { + pp = new PacketParser(handler); + packets = []; +} + +function execute(str: string, verify: () => void) { + reset(); + const buffers = str.split('|').map((sb) => sb.split(',').map(Number)); + for (let i = 0; i < buffers.length; ++i) { + pp.execute(Buffer.from(buffers[i])); + } + verify(); +} + +function p123() { + assert(packets.length === 1); + assert(packets[0].length() === 14); + assert(packets[0].sequenceId === 123); +} + +function p120_121() { + packets.forEach((p) => { + p.dump; + }); + assert(packets.length === 2); + assert(packets[0].length() === 4); + assert(packets[0].sequenceId === 120); + assert(packets[1].length() === 4); + assert(packets[1].sequenceId === 121); +} + +execute('10,0,0,123,1,2,3,4,5,6,7,8,9,0', p123); +execute('10,0,0,123|1,2,3,4,5,6,7,8,9,0', p123); +execute('10,0,0|123,1,2,3,4,5,6,7,8,9,0', p123); +execute('10|0,0|123,1,2,3,4,5,6,7,8,9,0', p123); +execute('10,0,0,123,1|2,3,4,5,6|7,8,9,0', p123); +execute('10,0,0,123,1,2|,3,4,5,6|7,8,9,0', p123); + +function p42() { + assert(packets.length === 1); + assert(packets[0].length() === 4); + assert(packets[0].sequenceId === 42); +} + +execute('0,0,0,42', p42); +execute('0|0,0,42', p42); +execute('0,0|0,42', p42); +execute('0,0|0|42', p42); +execute('0,0,0|42', p42); +execute('0|0|0|42', p42); +execute('0|0,0|42', p42); + +// two zero length packets +execute('0,0,0,120,0,0,0,121', p120_121); +execute('0,0,0|120|0|0|0|121', p120_121); + +const p122_123 = function () { + assert(packets.length === 2); + assert(packets[0].length() === 9); + assert(packets[0].sequenceId === 122); + assert(packets[1].length() === 10); + assert(packets[1].sequenceId === 123); +}; +// two non-zero length packets +execute('5,0,0,122,1,2,3,4,5,6,0,0,123,1,2,3,4,5,6', p122_123); +execute('5,0,0,122,1,2,3,4,5|6,0,0,123,1,2,3,4,5,6', p122_123); +execute('5,0,0,122,1,2,3,4|5|6|0,0,123,1,2,3,4,5,6', p122_123); +execute('5,0,0,122,1,2,3,4,5,6|0,0,123,1,2,3,4,5,6', p122_123); +execute('5,0,0,122,1,2,3,4,5,6,0|0,123,1,2,3,4,5,6', p122_123); +execute('5,0,0,122,1,2,3,4,5,6,0,0|123,1,2,3,4,5,6', p122_123); +execute('5,0,0,122,1,2,3,4,5,6,0,0,123|1,2,3,4,5,6', p122_123); +execute('5,0,0,122,1,2,3,4,5,6,0,0,123,1|2,3,4,5,6', p122_123); +execute('5,0,0,122,1,2,3,4,5,6,0,0,123,1|2,3|4,5,6', p122_123); + +// test packet > 65536 lengt +// TODO combine with "execute" function + +const length = 123000; +const pbuff = Buffer.alloc(length + 4); +pbuff[4] = 123; +pbuff[5] = 124; +pbuff[6] = 125; +const p = new Packet(144, pbuff, 4, pbuff.length - 4); +p.writeHeader(42); + +function testBigPackets( + chunks: Buffer[], + cb: (packets: PacketInstance[]) => void +) { + const packets: PacketInstance[] = []; + const pp = new PacketParser((p: PacketInstance) => { + packets.push(p); + }); + chunks.forEach((ch) => { + pp.execute(ch); + }); + cb(packets); +} + +function assert2FullPackets(packets: PacketInstance[]) { + function assertPacket(p: PacketInstance) { + assert.equal(p.length(), length + 4); + assert.equal(p.sequenceId, 42); + assert.equal(p.readInt8(), 123); + assert.equal(p.readInt8(), 124); + assert.equal(p.readInt8(), 125); + } + // assert.equal(packets[0].buffer.slice(0, 8).toString('hex'), expectation); + // assert.equal(packets[1].buffer.slice(0, 8).toString('hex'), expectation); + assert.equal(packets.length, 2); + assertPacket(packets[0]); + assertPacket(packets[1]); +} + +// 2 full packets in 2 chunks +testBigPackets([pbuff, pbuff], assert2FullPackets); + +testBigPackets( + [pbuff.slice(0, 120000), pbuff.slice(120000, 123004), pbuff], + assert2FullPackets +); +const frameEnd = 120000; +testBigPackets( + [ + pbuff.slice(0, frameEnd), + Buffer.concat([pbuff.slice(frameEnd, 123004), pbuff]), + ], + assert2FullPackets +); +for (let frameStart = 1; frameStart < 100; frameStart++) { + testBigPackets( + [ + Buffer.concat([pbuff, pbuff.slice(0, frameStart)]), + pbuff.slice(frameStart, 123004), + ], + assert2FullPackets + ); +} diff --git a/test/globals.d.ts b/test/globals.d.ts new file mode 100644 index 0000000000..0205a01fa4 --- /dev/null +++ b/test/globals.d.ts @@ -0,0 +1,9 @@ +declare const Deno: { + version: { + deno: string; + }; +}; + +declare const Bun: { + version: string; +}; From 540779755166362c1d7fa73a69591ea665d5d1c9 Mon Sep 17 00:00:00 2001 From: wellwelwel <46850407+wellwelwel@users.noreply.github.com> Date: Tue, 17 Feb 2026 20:17:36 -0300 Subject: [PATCH 2/3] chore: revert `Poku` from canary to latest --- package-lock.json | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index e027f7c5e9..9c5d12caa8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "eslint-plugin-markdown": "^5.1.0", "eslint-plugin-prettier": "^5.5.5", "globals": "^17.0.0", - "poku": "^3.0.3-canary.8f374795", + "poku": "^3.0.2", "portfinder": "^1.0.38", "prettier": "^3.8.0", "tsx": "^4.21.0", @@ -4171,9 +4171,9 @@ "license": "MIT" }, "node_modules/poku": { - "version": "3.0.3-canary.8f374795", - "resolved": "https://registry.npmjs.org/poku/-/poku-3.0.3-canary.8f374795.tgz", - "integrity": "sha512-IjcOXmnFrDr6YblPFxm+QK1C6bdy6DIywcCHANUQW6X5XOMR6ha7M9ZwQRA726DsAaVkVCqTNzl5o1benK+zIw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/poku/-/poku-3.0.2.tgz", + "integrity": "sha512-vwNcV5Y+gEhq1Sxw34HTZ4rTudg7LgaEoyZ4m7CXH/k9JKTTwwnj9fZGrstbEBR2e2TCroNHqt6cVGBAg6zkAA==", "dev": true, "license": "MIT", "bin": { @@ -4182,7 +4182,7 @@ "engines": { "bun": ">=1.x.x", "deno": ">=1.x.x", - "node": ">=16.x.x", + "node": ">=14.x.x", "typescript": ">=5.x.x" }, "funding": { diff --git a/package.json b/package.json index 837637c6c3..cf91f98259 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "eslint-plugin-markdown": "^5.1.0", "eslint-plugin-prettier": "^5.5.5", "globals": "^17.0.0", - "poku": "^3.0.3-canary.8f374795", + "poku": "^3.0.2", "portfinder": "^1.0.38", "prettier": "^3.8.0", "tsx": "^4.21.0", From a00f22b9f630e75942f0a7582a999eaf89a09613 Mon Sep 17 00:00:00 2001 From: wellwelwel <46850407+wellwelwel@users.noreply.github.com> Date: Tue, 17 Feb 2026 20:17:58 -0300 Subject: [PATCH 3/3] chore: fix `ssl` wrong param --- test/esm/integration/connection/test-quit.test.mts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/esm/integration/connection/test-quit.test.mts b/test/esm/integration/connection/test-quit.test.mts index 3e5879b326..aaf500fa9e 100644 --- a/test/esm/integration/connection/test-quit.test.mts +++ b/test/esm/integration/connection/test-quit.test.mts @@ -27,7 +27,8 @@ const server = createServer( host: 'localhost', // @ts-expect-error: internal access port: server._port, - ssl: undefined, + // @ts-expect-error: TODO: implement typings + ssl: false, }); connection.query(queryCli, (err, _rows, _fields) => {