├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ ├── feature_request.yml │ └── support_request.yml └── workflows │ ├── checklist_validator.yml │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .sync ├── APP_VERSION ├── CHANGELOG.md ├── Hello ├── LICENSE ├── PHP_VERSION ├── README.md ├── bin ├── cluster ├── codeanalyze ├── codestyle ├── codestyle-fix ├── git-changelog ├── query └── test ├── composer.json ├── composer.lock ├── docker-compose-dev.yml ├── docker-compose.yml ├── packages └── rpm.spec ├── phpstan.neon ├── phpunit.xml ├── plugins └── .gitkeep ├── ruleset.xml ├── src ├── Config │ └── LogLevel.php ├── Exception │ ├── SQLQueryCommandMissing.php │ └── SQLQueryCommandNotSupported.php ├── Handler.php ├── Lib │ ├── CliArgsProcessor.php │ ├── CrashDetector.php │ ├── Metric.php │ ├── MetricThread.php │ └── QueryProcessor.php ├── Network │ ├── EventHandler.php │ └── Server.php ├── Plugin │ ├── Alias │ │ ├── Handler.php │ │ └── Payload.php │ ├── AlterColumn │ │ ├── Handler.php │ │ └── Payload.php │ ├── AlterDistributedTable │ │ ├── Handler.php │ │ └── Payload.php │ ├── AlterRenameTable │ │ ├── Handler.php │ │ └── Payload.php │ ├── Autocomplete │ │ ├── Handler.php │ │ └── Payload.php │ ├── Backup │ │ ├── Handler.php │ │ └── Payload.php │ ├── CliTable │ │ ├── Handler.php │ │ └── Payload.php │ ├── CreateCluster │ │ ├── Handler.php │ │ └── Payload.php │ ├── CreateTable │ │ ├── Handler.php │ │ ├── Payload.php │ │ └── WithEngineHandler.php │ ├── DistributedInsert │ │ ├── Handler.php │ │ └── Payload.php │ ├── Drop │ │ ├── Handler.php │ │ └── Payload.php │ ├── EmptyString │ │ ├── Handler.php │ │ └── Payload.php │ ├── EmulateElastic │ │ ├── AddAliasHandler.php │ │ ├── AddEntityHandler.php │ │ ├── AddTemplateHandler.php │ │ ├── BaseEntityHandler.php │ │ ├── CatHandler.php │ │ ├── ClusterKibanaHandler.php │ │ ├── CountInfoKibanaHandler.php │ │ ├── CreateTableHandler.php │ │ ├── FieldCapsHandler.php │ │ ├── FieldCapsHandlerHelper.php │ │ ├── FindEntityHandler.php │ │ ├── GetAliasesHandler.php │ │ ├── GetEntityHandler.php │ │ ├── ImportKibanaHandler.php │ │ ├── InitKibanaHandler.php │ │ ├── InvalidSourceKibanaHandler.php │ │ ├── KibanaSearch │ │ │ ├── Handler.php │ │ │ ├── Logic │ │ │ │ ├── Request │ │ │ │ │ ├── Aliasing.php │ │ │ │ │ ├── Executor.php │ │ │ │ │ ├── Factory.php │ │ │ │ │ ├── FieldDetecting.php │ │ │ │ │ ├── Filtering.php │ │ │ │ │ ├── Interfaces │ │ │ │ │ │ ├── FailableLogicInterface.php │ │ │ │ │ │ └── RequestLogicInterface.php │ │ │ │ │ └── Ordering.php │ │ │ │ └── Response │ │ │ │ │ ├── BaseLogic.php │ │ │ │ │ ├── ConcurrentFilterProcessing │ │ │ │ │ ├── FilterSet.php │ │ │ │ │ └── Processing.php │ │ │ │ │ ├── DisabledMetricAdding.php │ │ │ │ │ ├── Executor.php │ │ │ │ │ ├── Factory.php │ │ │ │ │ ├── HistogramExtending.php │ │ │ │ │ ├── Interfaces │ │ │ │ │ └── ResponseLogicInterface.php │ │ │ │ │ ├── Sorting │ │ │ │ │ ├── Metric │ │ │ │ │ │ ├── Calculator.php │ │ │ │ │ │ ├── CalculatorFactory.php │ │ │ │ │ │ ├── CountCalculator.php │ │ │ │ │ │ ├── MetricCalculatorInterface.php │ │ │ │ │ │ ├── MetricUpdaterInterface.php │ │ │ │ │ │ └── Operation.php │ │ │ │ │ ├── SortField.php │ │ │ │ │ └── Sorting.php │ │ │ │ │ └── UnmatchedFilterProcessing.php │ │ │ ├── NodeSet.php │ │ │ ├── RequestNode │ │ │ │ ├── AggNode.php │ │ │ │ ├── BaseNode.php │ │ │ │ ├── BaseRange.php │ │ │ │ ├── DateHistogram.php │ │ │ │ ├── DateRange.php │ │ │ │ ├── ExprNode.php │ │ │ │ ├── Factory.php │ │ │ │ ├── GroupExprNode.php │ │ │ │ ├── GroupFilter.php │ │ │ │ ├── Helpers │ │ │ │ │ ├── FilterExpression │ │ │ │ │ │ ├── Factory.php │ │ │ │ │ │ └── FilterExpression.php │ │ │ │ │ └── TimeZoneExpression.php │ │ │ │ ├── Histogram.php │ │ │ │ ├── Interfaces │ │ │ │ │ ├── AggNodeInterface.php │ │ │ │ │ ├── AliasedNodeInterface.php │ │ │ │ │ └── FilterNodeInterface.php │ │ │ │ ├── Metric.php │ │ │ │ ├── QueryFilter.php │ │ │ │ ├── Range.php │ │ │ │ └── Term.php │ │ │ ├── RequestParser.php │ │ │ ├── Response.php │ │ │ ├── SphinxQLRequest.php │ │ │ └── TableFieldInfo.php │ │ ├── LicenseHandler.php │ │ ├── ManagerSettingsKibanaHandler.php │ │ ├── MetricKibanaHandler.php │ │ ├── MgetKibanaHandler.php │ │ ├── NodesInfoKibanaHandler.php │ │ ├── Payload.php │ │ ├── QueryMap │ │ │ ├── Cluster.php │ │ │ ├── ManagerSettings.php │ │ │ └── Settings.php │ │ ├── QueryMapLoaderTrait.php │ │ ├── SettingsKibanaHandler.php │ │ ├── TableKibanaHandler.php │ │ ├── TelemetryKibanaHandler.php │ │ ├── UpdateEntityHandler.php │ │ └── XpackInfoKibanaHandler.php │ ├── Fuzzy │ │ ├── Handler.php │ │ └── Payload.php │ ├── Insert │ │ ├── Error │ │ │ ├── AutoSchemaDisabledError.php │ │ │ └── ParserLoadError.php │ │ ├── Handler.php │ │ ├── Payload.php │ │ └── QueryParser │ │ │ ├── BaseParser.php │ │ │ ├── CheckInsertDataTrait.php │ │ │ ├── Datalim.php │ │ │ ├── Datatype.php │ │ │ ├── ElasticJSONInsertParser.php │ │ │ ├── InsertQueryParserInterface.php │ │ │ ├── JSONInsertParser.php │ │ │ ├── JSONParser.php │ │ │ ├── JSONParserInterface.php │ │ │ ├── Loader.php │ │ │ ├── QueryParserInterface.php │ │ │ └── SQLInsertParser.php │ ├── Knn │ │ ├── Handler.php │ │ └── Payload.php │ ├── Metrics │ │ ├── Handler.php │ │ └── Payload.php │ ├── ModifyTable │ │ ├── Handler.php │ │ └── Payload.php │ ├── Plugin │ │ ├── ActionType.php │ │ ├── Handler.php │ │ └── Payload.php │ ├── Queue │ │ ├── Handlers │ │ │ ├── BaseDropHandler.php │ │ │ ├── BaseGetHandler.php │ │ │ ├── BaseViewHandler.php │ │ │ ├── Source │ │ │ │ ├── BaseCreateSourceHandler.php │ │ │ │ ├── CreateKafka.php │ │ │ │ ├── DropSourceHandler.php │ │ │ │ ├── GetSourceHandler.php │ │ │ │ └── ViewSourceHandler.php │ │ │ └── View │ │ │ │ ├── AlterViewHandler.php │ │ │ │ ├── CreateViewHandler.php │ │ │ │ ├── DropViewHandler.php │ │ │ │ ├── GetViewHandler.php │ │ │ │ └── ViewViewsHandler.php │ │ ├── Models │ │ │ ├── Alter │ │ │ │ └── AlterMaterializedViewModel.php │ │ │ ├── Create │ │ │ │ ├── CreateMaterializedViewModel.php │ │ │ │ └── CreateSourceModel.php │ │ │ ├── Drop │ │ │ │ ├── DropMaterializedViewModel.php │ │ │ │ └── DropSourceModel.php │ │ │ ├── Factories │ │ │ │ ├── AlterFactory.php │ │ │ │ ├── CreateFactory.php │ │ │ │ ├── DropFactory.php │ │ │ │ └── ShowFactory.php │ │ │ ├── Model.php │ │ │ ├── Show │ │ │ │ ├── ShowMaterializedViewModel.php │ │ │ │ ├── ShowMaterializedViewsModel.php │ │ │ │ ├── ShowSourceModel.php │ │ │ │ └── ShowSourcesModel.php │ │ │ └── SqlModelsHandler.php │ │ ├── Payload.php │ │ ├── QueueProcess.php │ │ ├── StringFunctionsTrait.php │ │ └── Workers │ │ │ └── Kafka │ │ │ ├── Batch.php │ │ │ ├── KafkaWorker.php │ │ │ └── View.php │ ├── Replace │ │ ├── Handler.php │ │ └── Payload.php │ ├── Select │ │ ├── Handler.php │ │ └── Payload.php │ ├── Sharding │ │ ├── Cluster.php │ │ ├── CreateHandler.php │ │ ├── DescHandler.php │ │ ├── DropHandler.php │ │ ├── Node.php │ │ ├── Operator.php │ │ ├── Payload.php │ │ ├── Processor.php │ │ ├── Queue.php │ │ ├── State.php │ │ ├── Table.php │ │ ├── TableOperation.php │ │ └── Util.php │ ├── Show │ │ ├── CreateTableHandler.php │ │ ├── ExpandedTablesHandler.php │ │ ├── FullColumnsHandler.php │ │ ├── Payload.php │ │ ├── QueriesHandler.php │ │ ├── SchemasHandler.php │ │ ├── UnsupportedStmtHandler.php │ │ └── VersionHandler.php │ ├── Test │ │ ├── Handler.php │ │ └── Payload.php │ ├── Truncate │ │ ├── Handler.php │ │ └── Payload.php │ └── Update │ │ ├── Handler.php │ │ └── Payload.php ├── func.php ├── init.php └── main.php └── test ├── Buddy ├── functional │ ├── AutoSchemaSupportTest.php │ ├── BackupTest.php │ ├── BenchLoadTest.php │ ├── BuildTest.php │ ├── CliTableTest.php │ ├── DebugModeTest.php │ ├── DirectRequestTest.php │ ├── HungRequestTest.php │ ├── InsertQueryTest.php │ ├── ListenArgTest.php │ ├── MetricThreadTest.php │ ├── MultipleQueriesTest.php │ ├── OnStartOutputTest.php │ ├── ProcessErrorTest.php │ ├── ProcessKillTest.php │ ├── ShowFullTablesTest.php │ ├── ShowOpenTablesTest.php │ ├── ShowQueriesTest.php │ ├── ShowVariablesTest.php │ └── config │ │ ├── manticore-bench.conf │ │ └── manticore.conf └── src │ ├── Lib │ ├── CliArgsProcessorTest.php │ └── QueryProcessorTest.php │ ├── Sharding │ └── UtilTest.php │ └── Trait │ └── CheckInsertDataTraitTest.php ├── Kafka ├── dump.json └── import.sh ├── Plugin ├── Backup │ └── BackupPayloadTest.php ├── CliTable │ └── CliTableHandlerTest.php ├── EmulateElastic │ ├── CreateTableHandlerTest.php │ └── PayloadTest.php ├── Insert │ ├── Exception │ │ └── ParserLoadErrorTest.php │ ├── InsertDataCheckTest.php │ ├── InsertQuery │ │ ├── InsertQueryHandlerTest.php │ │ └── InsertQueryPayloadTest.php │ └── QueryParser │ │ ├── JSONInsertParserTest.php │ │ ├── ParserLoaderTest.php │ │ └── SQLInsertParserTest.php ├── Sharding │ └── NodeTest.php └── Show │ ├── ShowFullOrOpenTables │ └── ShowFullOrOpenTablesPayloadTest.php │ └── ShowQueries │ └── ShowQueriesHandlerTest.php ├── bootstrap.php └── src ├── Lib ├── BuddyRequestError.php ├── MockManticoreServer.php └── SocketError.php └── Trait ├── TestFunctionalTrait.php ├── TestHTTPServerTrait.php └── TestProtectedTrait.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [**.php] 13 | indent_style = tab 14 | indent_size = 2 15 | 16 | [**.yaml] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [**.yml] 21 | indent_style = space 22 | indent_size = 2 23 | 24 | [**.md] 25 | indent_style = space 26 | indent_size = 2 27 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | APP_VERSION export-subst 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug Report 2 | description: Submit a bug report for Manticore Buddy 3 | labels: bug 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for submitting a bug report. We appreciate your effort to provide detailed information. Please answer the following questions to help us identify and fix the bug. Thank you! 9 | - type: textarea 10 | id: proposal 11 | attributes: 12 | label: "Bug Description:" 13 | description: > 14 | Describe the bug in detail. Include a [Minimal Reproducible Example](https://en.wikipedia.org/wiki/Minimal_reproducible_example) (MRE) if possible. Place any code blocks within triple backticks: 15 | value: | 16 | ```bash 17 | # Example code block; replace with your code if applicable 18 | ``` 19 | validations: 20 | required: true 21 | - type: input 22 | id: version 23 | attributes: 24 | label: "Manticore Search Version:" 25 | description: > 26 | Provide the version of Manticore Search you are using. Execute `searchd -v` in the command line to find this information. 27 | validations: 28 | required: true 29 | - type: input 30 | id: os 31 | attributes: 32 | label: "Operating System Version:" 33 | description: > 34 | Specify the version of your operating system. 35 | validations: 36 | required: true 37 | - type: dropdown 38 | id: dev 39 | attributes: 40 | label: "Have you tried the latest development version?" 41 | multiple: false 42 | options: 43 | - "Yes" 44 | - "No" 45 | - type: markdown 46 | attributes: 47 | value: "## Thank you for completing the form! For an expedited solution, consider our [professional services](https://manticoresearch.com/services/)." 48 | - type: textarea 49 | id: checklist 50 | attributes: 51 | label: "Internal Checklist:" 52 | description: > 53 | **For Manticore Team Use Only** — Please do not edit this section. This checklist will be completed by the Manticore team as they manage the issue. 54 | value: | 55 | To be completed by the assignee. Check off tasks that have been completed or are not applicable. 56 |
57 | 58 | - [ ] Implementation completed 59 | - [ ] Tests developed 60 | - [ ] Documentation updated 61 | - [ ] Documentation reviewed 62 | 63 |
64 | validations: 65 | required: true 66 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: "Manticore Team's professional services" 3 | about: "Looking for a faster solution to your issues with Manticore? Manticore Team can help." 4 | url: "https://manticoresearch.com/services" 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 🌟 Feature Request 2 | description: Submit a proposal for a new Manticore Buddy feature or enhancement 3 | body: 4 | - type: textarea 5 | id: proposal 6 | attributes: 7 | label: "Proposal:" 8 | description: > 9 | Please describe your proposal in detail. 10 | Include why you believe this feature should be added to Manticore Buddy and what use cases it supports. 11 | If applicable, add any examples or code snippets inside triple backticks to clarify your proposal. 12 | validations: 13 | required: true 14 | - type: markdown 15 | attributes: 16 | value: "## Thank you for completing the form! If you are interested in sponsoring the development of this feature, consider our [professional services](https://manticoresearch.com/services/)." 17 | - type: textarea 18 | id: checklist 19 | attributes: 20 | label: "Checklist:" 21 | description: > 22 | **For Manticore Team Use Only** — Please do not edit this section. This checklist will be completed by the Manticore team as they manage the issue. 23 | value: | 24 | To be completed by the assignee. Check off tasks that have been completed or are not applicable. 25 |
26 | 27 | - [ ] Implementation completed 28 | - [ ] Tests developed 29 | - [ ] Documentation updated 30 | - [ ] Documentation reviewed 31 | - [x] OpenAPI YAML updated and issue created to rebuild clients 32 | 33 |
34 | validations: 35 | required: true 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/support_request.yml: -------------------------------------------------------------------------------- 1 | name: ❓ Support Request 2 | description: Need help with Manticore? Submit your questions here! 3 | body: 4 | - type: checkboxes 5 | id: dev 6 | attributes: 7 | label: "Confirmation Checklist:" 8 | description: > 9 | Before submitting your request, we ask that you confirm the following items to ensure that you receive the most effective support: 10 | options: 11 | - label: "You have searched for an answer in [the manual](https://manual.manticoresearch.com/)." 12 | - label: "You have considered using [the forum](https://forum.manticoresearch.com/) for general discussions, which can be more suitable for non-urgent or broad queries." 13 | - label: "You are aware of our community support channels on [Slack](https://slack.manticoresearch.com/), [Telegram EN](https://t.me/manticoresearch_en), and [Telegram RU](https://t.me/manticore_chat), where you can interact with other users and our developers." 14 | - label: "You know about Manticore Team's [professional services](https://manticoresearch.com/services). Engaging with our experts through a support subscription can significantly accelerate resolution times and provide tailored solutions to your specific needs." 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: question 19 | attributes: 20 | label: "Your question:" 21 | description: > 22 | Please provide detailed information about your question to ensure prompt and accurate help! 23 | validations: 24 | required: true 25 | - type: markdown 26 | attributes: 27 | value: "## Thank you for completing the form! If you need immediate, dedicated support or a long-term support subscription, consider using our [professional services](https://manticoresearch.com/services)." 28 | -------------------------------------------------------------------------------- /.github/workflows/checklist_validator.yml: -------------------------------------------------------------------------------- 1 | name: 📝 Checklist Validator 2 | run-name: 📝 Checklist Validator for issue ${{ github.event.issue.number }} 3 | 4 | on: 5 | issues: 6 | types: 7 | - closed 8 | 9 | jobs: 10 | checklist-validation: 11 | name: ✅ Checklist Completion Check 12 | runs-on: ubuntu-22.04 13 | steps: 14 | - uses: manticoresoftware/manticoresearch/actions/checklist-validator@master 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/** 2 | tmp/** 3 | build/** 4 | .phpunit.result.cache 5 | /.ptp-sync/ 6 | .ptp-sync-folder 7 | phar_builder 8 | plugins/* 9 | !plugins/.gitkeep 10 | bin/manticore-*.conf 11 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v2.3.0 6 | hooks: 7 | - id: check-yaml 8 | - id: end-of-file-fixer 9 | - id: trailing-whitespace 10 | - id: check-executables-have-shebangs 11 | - id: check-added-large-files 12 | - repo: local 13 | hooks: 14 | - id: codestyle-fix 15 | name: codestyle-fix 16 | entry: bin/codestyle-fix 17 | language: system 18 | types: [php] 19 | pass_filenames: false 20 | - id: codestyle 21 | name: codestyle 22 | entry: bin/codestyle 23 | language: system 24 | types: [php] 25 | pass_filenames: false 26 | - id: codeanalyze 27 | name: codeanalyze 28 | entry: bin/codeanalyze 29 | language: system 30 | types: [php] 31 | pass_filenames: false 32 | -------------------------------------------------------------------------------- /.sync: -------------------------------------------------------------------------------- 1 | vendor/* 2 | plugins/* 3 | -------------------------------------------------------------------------------- /APP_VERSION: -------------------------------------------------------------------------------- 1 | 3.30.2 2 | -------------------------------------------------------------------------------- /Hello: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticoresoftware/manticoresearch-buddy/c16d9c261ef4b3b656fe1c2d4e8f846e8f42f3c4/Hello -------------------------------------------------------------------------------- /PHP_VERSION: -------------------------------------------------------------------------------- 1 | 8.1 2 | -------------------------------------------------------------------------------- /bin/cluster: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | if [ -z "$1" ]; then 5 | echo "Usage: $0 " 6 | exit 1 7 | fi 8 | nodes=$1 9 | if ((nodes < 2)); then 10 | echo "Number of nodes must be greater than 1" 11 | exit 1 12 | fi 13 | if ((nodes > 5)); then 14 | echo "Number of nodes must be less than or equal to 5" 15 | exit 1 16 | fi 17 | 18 | for n in $(seq 1 $nodes); do 19 | cat << EOF > $DIR/manticore-${n}.conf 20 | searchd { 21 | buddy_path = manticore-executor /workdir/src/main.php --log-level=debugvv 22 | listen = 127.0.0.1:${n}9312 23 | listen = 127.0.0.1:${n}9306:mysql 24 | listen = 127.0.0.1:${n}9308:http 25 | log = /var/log/manticore/searchd-$n.log 26 | query_log = /var/log/manticore/query-$n.log 27 | pid_file = /var/run/manticore/searchd-$n.pid 28 | data_dir = /var/lib/manticore/$n 29 | } 30 | EOF 31 | done 32 | 33 | # Function to stop processes 34 | stop_processes() { 35 | echo "Stopping searchd processes..." 36 | 37 | for n in $(seq 1 $nodes); do 38 | searchd --config "$DIR/manticore-${n}.conf" --stop 39 | done 40 | exit 0 41 | } 42 | 43 | # Set up trap to catch Cmd+C (SIGINT) 44 | trap stop_processes SIGINT 45 | 46 | # Start both searchd processes in the background and redirect output to console 47 | for n in $(seq 1 $nodes); do 48 | test -d /var/lib/manticore/$n && rm -rf $_ 49 | mkdir -p /var/lib/manticore/$n 50 | searchd --config "$DIR/manticore-${n}.conf" --nodetach > >(sed 's/^/['$n'] /') 2>&1 & 51 | done 52 | 53 | # Wait for all searchd processes to start 54 | sleep 2 55 | 56 | # Creating cluster 57 | mysql -h0 -P19306 -e 'CREATE CLUSTER c' 58 | for n in $(seq 2 $nodes); do 59 | mysql -h0 -P"${n}9306" -e "JOIN CLUSTER c at '127.0.0.1:19312'" 60 | done 61 | 62 | echo "All searchd processes started. Press Cmd+C to stop." 63 | 64 | # Wait indefinitely 65 | while true; do 66 | sleep 1 67 | done 68 | -------------------------------------------------------------------------------- /bin/codeanalyze: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ./vendor/bin/phpstan --memory-limit=-1 analyse -c "$(pwd)/phpstan.neon" 3 | -------------------------------------------------------------------------------- /bin/codestyle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ./vendor/bin/phpcs -v --no-cache -s --standard="$(pwd)/ruleset.xml" src/ test/ 3 | -------------------------------------------------------------------------------- /bin/codestyle-fix: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ./vendor/bin/phpcbf -v --no-cache -s --standard="$(pwd)/ruleset.xml" src/ test/ 3 | -------------------------------------------------------------------------------- /bin/git-changelog: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo '## Changes' 4 | echo 5 | 6 | readarray -t pair < <(git tag --sort=-version:refname | head -n 2) 7 | readarray -t lines < <(git --no-pager log --format="%cI %H [%cN] %s" "${pair[1]}..${pair[0]}") 8 | 9 | for line in "${lines[@]}"; do 10 | date=$(echo "$line" | cut -d' ' -f1) 11 | commit=$(echo "$line" | cut -d' ' -f2) 12 | # author=$(echo "$line" | cut -d' ' -f3) 13 | message=$(echo "$line" | cut -d' ' -f4) 14 | 15 | repl="**${date}**" 16 | line=${line//$date/$repl} 17 | 18 | repl="[${commit:0:7}](https://github.com/manticoresoftware/manticoresearch-backup/commit/${commit})" 19 | line=${line//$commit/$repl} 20 | 21 | repl="*${message}*" 22 | line=${line//$message/$repl} 23 | 24 | echo "$line" 25 | done 26 | -------------------------------------------------------------------------------- /bin/query: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | query="$1" 3 | if [[ -z "$query" ]]; then 4 | echo >&2 "Usage: $0 [query]" 5 | exit 1 6 | fi 7 | 8 | executor=$(which manticore-executor 2> /dev/null || which php 2> /dev/null) 9 | if [[ -z "$executor" ]]; then 10 | echo >&2 'You should install manticore-executor or PHP' 11 | exit 1 12 | fi 13 | 14 | query=${query//\'/\\\'} 15 | $executor -n < Buddy::PROTOCOL_VERSION, 29 | 'type' => 'unknown json request', 30 | 'error' => '', 31 | 'message' => [ 32 | 'path_query' => '/cli', 33 | 'body' => '$query', 34 | ] 35 | ]); 36 | \$task = QueryProcessor::process(\$request)->run(); 37 | \$status = \$task->wait(true); 38 | printf('Status code: %s' . PHP_EOL, \$status->name); 39 | printf('Result: ' . PHP_EOL . '%s', \$task->getResult()->getStruct()); 40 | } catch (Throwable \$e) { 41 | echo 'Error:' . PHP_EOL; 42 | echo ' ' . \$e::class . ': ' . \$e->getMessage() . PHP_EOL; 43 | exit(1); 44 | } 45 | echo 'done' . PHP_EOL; 46 | 47 | CODE 48 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ./vendor/bin/phpunit -d memory_limit=4G --stop-on-failure "$@" test/ 3 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "manticoresoftware/manticoresearch-buddy", 3 | "description": "Buddy assistant for the Manticore Search", 4 | "keywords": [ 5 | "search", 6 | "backup", 7 | "manticoresearch" 8 | ], 9 | "license": "GPL-2.0-or-later", 10 | "type": "project", 11 | "config": { 12 | "platform": { 13 | "php": "8.1.0" 14 | }, 15 | "allow-plugins": { 16 | "dealerdirect/phpcodesniffer-composer-installer": true 17 | } 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "Manticoresearch\\Buddy\\Base\\": "src/" 22 | }, 23 | "files": ["src/func.php"] 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "Manticoresearch\\BuddyTest\\": [ 28 | "test/src" 29 | ] 30 | } 31 | }, 32 | "require": { 33 | "manticoresoftware/telemetry": "^0.1.19", 34 | "symfony/dependency-injection": "^6.1", 35 | "manticoresoftware/buddy-core": "dev-main", 36 | "php-ds/php-ds": "^1.4", 37 | "manticoresoftware/manticoresearch-backup": "^1.3", 38 | "symfony/expression-language": "^6.4" 39 | }, 40 | "require-dev": { 41 | "phpstan/phpstan": "^1.8", 42 | "slevomat/coding-standard": "^8.5", 43 | "squizlabs/php_codesniffer": "^3.7", 44 | "phpunit/phpunit": "^9.5", 45 | "kwn/php-rdkafka-stubs": "^2.2", 46 | "swoole/ide-helper": "~5.0.0" 47 | }, 48 | "repositories": [ 49 | { 50 | "type": "path", 51 | "url": "./plugins/*" 52 | } 53 | ], 54 | "bin": ["manticore-buddy"] 55 | } 56 | -------------------------------------------------------------------------------- /docker-compose-dev.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | kafka: 5 | image: docker.io/bitnami/kafka:3.7 6 | profiles: [queues] 7 | container_name: kafka 8 | networks: 9 | - app-network 10 | volumes: 11 | - ./test/Kafka/import.sh:/import.sh 12 | - ./test/Kafka/dump.json:/tmp/dump.json 13 | environment: 14 | # KRaft settings 15 | - KAFKA_CFG_NODE_ID=0 16 | - KAFKA_CFG_PROCESS_ROLES=controller,broker 17 | - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka:9093 18 | # Listeners 19 | - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093 20 | - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://:9092 21 | - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT 22 | - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER 23 | - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT 24 | buddy: 25 | image: ghcr.io/manticoresoftware/manticoresearch:test-kit-latest 26 | container_name: manticore-buddy 27 | privileged: true 28 | tty: true 29 | entrypoint: 30 | - "/bin/sh" 31 | - "-c" 32 | - "grep buddy_path /etc/manticoresearch/manticore.conf > /dev/null 2>&1 || sed -i '/searchd {/a \\ buddy_path = manticore-executor /workdir/src/main.php --debugvv' /etc/manticoresearch/manticore.conf && sed -i '/^searchd {/a \\ listen = /var/run/mysqld/mysqld.sock:mysql41' /etc/manticoresearch/manticore.conf; exec /bin/bash" 33 | working_dir: "/workdir" 34 | networks: 35 | - app-network 36 | volumes: 37 | - ./:/workdir/ 38 | networks : 39 | app-network : 40 | driver : bridge 41 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | manticore-executor: 4 | cap_add: 5 | - SYS_ADMIN 6 | build: 7 | context: . 8 | args: 9 | TARGET_ARCH: amd64 10 | working_dir: /var/www 11 | volumes: 12 | - .:/var/www 13 | -------------------------------------------------------------------------------- /packages/rpm.spec: -------------------------------------------------------------------------------- 1 | Summary: {{ DESC }} 2 | Name: {{ NAME }} 3 | Version: {{ VERSION }} 4 | Release: 1%{?dist} 5 | Group: Applications 6 | License: GPLv2 7 | Packager: {{ MAINTAINER }} 8 | Vendor: {{ MAINTAINER }} 9 | Requires: {{ LIBCURL_NAME }} >= {{ LIBCURL_VERSION }} 10 | 11 | Source: tmp.tar.gz 12 | BuildRoot: %{_tmppath}/%{name}-%{version}-buildroot 13 | BuildArch: noarch 14 | 15 | %description 16 | {{ DESC }} 17 | 18 | %prep 19 | rm -rf %{buildroot} 20 | 21 | %setup -n %{name} 22 | 23 | %build 24 | 25 | %install 26 | mkdir -p %{buildroot}/usr/share/manticore/modules 27 | cp -rp usr/share/manticore/modules/{{ NAME }} %{buildroot}/usr/share/manticore/modules/{{ NAME }} 28 | 29 | %clean 30 | rm -rf %{buildroot} 31 | 32 | %post 33 | 34 | %postun 35 | 36 | %files 37 | %defattr(-, root, root) 38 | %dir /usr/share/manticore/modules/{{ NAME }} 39 | %dir /usr/share/manticore/modules/{{ NAME }}/bin 40 | /usr/share/manticore/modules/{{ NAME }}/src/* 41 | /usr/share/manticore/modules/{{ NAME }}/vendor/* 42 | /usr/share/manticore/modules/{{ NAME }}/APP_VERSION 43 | /usr/share/manticore/modules/{{ NAME }}/composer.json 44 | /usr/share/manticore/modules/{{ NAME }}/composer.lock 45 | %attr(1755, root, root) /usr/share/manticore/modules/{{ NAME }}/bin/{{ NAME }} 46 | 47 | %changelog 48 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | paths: 3 | - src 4 | - test 5 | level: 9 6 | inferPrivatePropertyTypeFromConstructor: true 7 | checkGenericClassInNonGenericObjectType: true 8 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | src/ 6 | 7 | 8 | test/ 9 | 10 | 11 | 12 | test/ 13 | 14 | 15 | -------------------------------------------------------------------------------- /plugins/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticoresoftware/manticoresearch-buddy/c16d9c261ef4b3b656fe1c2d4e8f846e8f42f3c4/plugins/.gitkeep -------------------------------------------------------------------------------- /src/Config/LogLevel.php: -------------------------------------------------------------------------------- 1 | 'info', 21 | self::Debug => 'debug', 22 | self::Debugv => 'debugv', 23 | self::Debugvv => 'debugvv', 24 | }; 25 | } 26 | 27 | /** 28 | * Create an enum instance from a string name 29 | * @param string $name 30 | * @return static 31 | */ 32 | public static function fromString(string $name): static { 33 | return match (strtolower($name)) { 34 | 'info' => self::Info, 35 | 'debug' => self::Debug, 36 | 'debugv' => self::Debugv, 37 | 'debugvv' => self::Debugvv, 38 | default => throw new \InvalidArgumentException("Invalid log level $name"), 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Exception/SQLQueryCommandMissing.php: -------------------------------------------------------------------------------- 1 | query); 40 | if (!$query) { 41 | throw new RuntimeException('Failed to prepare query'); 42 | } 43 | 44 | $resp = $manticoreClient 45 | ->sendRequest($query, $payload->path); 46 | return TaskResult::fromResponse($resp); 47 | }; 48 | 49 | return Task::create( 50 | $taskFn, 51 | [$this->payload, $this->manticoreClient] 52 | )->run(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Plugin/Alias/Handler.php: -------------------------------------------------------------------------------- 1 | query); 40 | if (!$query) { 41 | throw new RuntimeException('Failed to prepare query'); 42 | } 43 | 44 | $queryResponse = $manticoreClient 45 | ->sendRequest($query, $payload->path); 46 | return TaskResult::fromResponse($queryResponse); 47 | }; 48 | 49 | return Task::create( 50 | $taskFn, 51 | [$this->payload, $this->manticoreClient] 52 | )->run(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Plugin/Alias/Payload.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | final class Payload extends BasePayload { 21 | /** @var string */ 22 | public string $path; 23 | 24 | /** @var string */ 25 | public string $query; 26 | 27 | public function __construct() { 28 | } 29 | 30 | /** 31 | * @param Request $request 32 | * @return static 33 | */ 34 | public static function fromRequest(Request $request): static { 35 | $self = new static(); 36 | $self->path = $request->path; 37 | $self->query = $request->payload; 38 | return $self; 39 | } 40 | 41 | /** 42 | * @param Request $request 43 | * @return bool 44 | */ 45 | public static function hasMatch(Request $request): bool { 46 | $hasError = stripos($request->error, 'near') !== false && ( 47 | str_contains($request->error, "unexpected \$undefined near '.*") 48 | || str_contains($request->error, "unexpected identifier, expecting SET near 't ") 49 | || ( 50 | str_contains($request->error, "expecting \$end near '") 51 | && str_contains($request->error, " t'") 52 | ) 53 | ); 54 | if (!$hasError) { 55 | return false; 56 | } 57 | 58 | return stripos($request->payload, ' t.') !== false 59 | || str_ends_with($request->payload, ' t'); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Plugin/Backup/Handler.php: -------------------------------------------------------------------------------- 1 | payload->options['async'] ?? false; 45 | $task = Task::create( 46 | static function (string $args): TaskResult { 47 | /** @var Payload $payload */ 48 | /** @phpstan-ignore-next-line */ 49 | [$payload] = unserialize($args); 50 | $config = new ManticoreConfig($payload->configPath); 51 | $client = new ManticoreClient([$config]); 52 | $storage = new FileStorage( 53 | $payload->path, 54 | $payload->options['compress'] ?? false 55 | ); 56 | ManticoreBackup::run('store', [$client, $storage, $payload->tables]); 57 | ; 58 | return TaskResult::withRow( 59 | [ 60 | 'Path' => $storage->getBackupPaths()['root'], 61 | ] 62 | )->column('Path', Column::String); 63 | }, 64 | [serialize([$this->payload])] 65 | ); 66 | if ($isAsync) { 67 | $task->defer(); 68 | } 69 | return $task->run(); 70 | } 71 | 72 | /** 73 | * @return array 74 | */ 75 | public function getProps(): array { 76 | return []; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Plugin/CliTable/Handler.php: -------------------------------------------------------------------------------- 1 | sendRequest( 48 | $payload->query, 49 | $payload->path, 50 | disableAgentHeader: true 51 | ); 52 | return TaskResult::fromResponse($resp); 53 | }; 54 | 55 | return Task::create( 56 | $taskFn, 57 | [$this->payload, $this->manticoreClient] 58 | )->run(); 59 | } 60 | 61 | /** 62 | * @param array $resultInfo 63 | * @param array> $data 64 | * @param int $total 65 | * @return void 66 | */ 67 | protected static function processResultInfo(array $resultInfo, ?array &$data = [], int &$total = -1): void { 68 | if (isset($resultInfo['data']) && is_array($resultInfo['data'])) { 69 | $data = $resultInfo['data']; 70 | } 71 | if (!isset($resultInfo['total'])) { 72 | return; 73 | } 74 | $total = $resultInfo['total']; 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/Plugin/CliTable/Payload.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | final class Payload extends BasePayload { 23 | public string $query; 24 | public string $path; 25 | 26 | /** 27 | * Get description for this plugin 28 | * @return string 29 | */ 30 | public static function getInfo(): string { 31 | return '/cli endpoint based on /cli_json - outputs query result as a table'; 32 | } 33 | 34 | /** 35 | * @param Request $request 36 | * @return static 37 | */ 38 | public static function fromRequest(Request $request): static { 39 | $self = new static(); 40 | $self->query = $request->payload; 41 | $self->path = ManticoreEndpoint::Sql->value; 42 | return $self; 43 | } 44 | 45 | /** 46 | * @param Request $request 47 | * @return bool 48 | */ 49 | public static function hasMatch(Request $request): bool { 50 | return $request->endpointBundle === ManticoreEndpoint::Cli; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Plugin/CreateCluster/Handler.php: -------------------------------------------------------------------------------- 1 | manticoreClient->sendRequest($this->payload->query); 39 | $error = $resp->getError(); 40 | // In case quiet mode we have IF EXISTS so do nothing 41 | if ($error && $this->payload->quiet) { 42 | return TaskResult::none(); 43 | } 44 | return TaskResult::fromResponse($resp); 45 | }; 46 | 47 | return Task::create( 48 | $taskFn, [$this->payload, $this->manticoreClient] 49 | )->run(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Plugin/CreateCluster/Payload.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | final class Payload extends BasePayload { 22 | public string $query; 23 | public bool $quiet = false; 24 | 25 | /** 26 | * Get description for this plugin 27 | * @return string 28 | */ 29 | public static function getInfo(): string { 30 | return 'Enable CREATE CLUSTER IF NOT EXISTS statements'; 31 | } 32 | 33 | /** 34 | * @param Request $request 35 | * @return static 36 | */ 37 | public static function fromRequest(Request $request): static { 38 | $self = new static(); 39 | 40 | $payload = $request->payload; 41 | $self->query = $payload; 42 | if (preg_match('/IF\s+NOT\s+EXISTS/ius', $payload)) { 43 | $self->query = preg_replace('/\s+IF\s+NOT\s+EXISTS/ius', '', $payload) ?: $payload; 44 | $self->quiet = true; 45 | } 46 | 47 | return $self; 48 | } 49 | 50 | /** 51 | * @param Request $request 52 | * @return bool 53 | * @throws GenericError 54 | */ 55 | public static function hasMatch(Request $request): bool { 56 | return $request->command === 'create' 57 | && stripos($request->payload, 'create cluster') === 0 58 | && stripos($request->error, 'P03') !== false; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Plugin/CreateTable/WithEngineHandler.php: -------------------------------------------------------------------------------- 1 | destinationTableName}"; 43 | $result = $client->sendRequest($sql); 44 | if ($result->hasError()) { 45 | throw GenericError::create( 46 | "Can't create table {$payload->destinationTableName}. Reason: " . $result->getError() 47 | ); 48 | } 49 | return TaskResult::none(); 50 | }; 51 | 52 | return Task::create( 53 | $taskFn, [$this->payload, $this->manticoreClient] 54 | )->run(); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/Plugin/Drop/Handler.php: -------------------------------------------------------------------------------- 1 | table}"; 41 | $result = $client->sendRequest($stmt); 42 | if ($result->hasError()) { 43 | throw GenericError::create( 44 | "Can't drop table {$payload->table}" . 45 | "Reason: {$result->getError()}" 46 | ); 47 | } 48 | 49 | return TaskResult::none(); 50 | }; 51 | 52 | return Task::create( 53 | $taskFn, [$this->payload, $this->manticoreClient] 54 | )->run(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Plugin/Drop/Payload.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | final class Payload extends BasePayload 24 | { 25 | public string $table; 26 | 27 | /** 28 | * Get description for this plugin 29 | * @return string 30 | */ 31 | public static function getInfo(): string { 32 | return 'Handles DROP statements with MySQL options not supported by Manticore'; 33 | } 34 | 35 | /** 36 | * @param Request $request 37 | * @return static 38 | */ 39 | public static function fromRequest(Request $request): static { 40 | $self = new static(); 41 | 42 | $matches = []; 43 | if (preg_match('/(Manticore|`Manticore`)\.(\S+)\s*$/i', $request->payload, $matches)) { 44 | $self->table = $matches[2]; 45 | return $self; 46 | } 47 | 48 | throw QueryParseError::create('Failed to handle your DROP query', true); 49 | } 50 | 51 | /** 52 | * @param Request $request 53 | * @return bool 54 | */ 55 | public static function hasMatch(Request $request): bool { 56 | return (stripos($request->error, "P01: syntax error, unexpected identifier near 'DROP TABLE") === 0); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Plugin/EmptyString/Handler.php: -------------------------------------------------------------------------------- 1 | run(); 40 | } 41 | 42 | /** 43 | * @return array 44 | */ 45 | public function getProps(): array { 46 | return []; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Plugin/EmptyString/Payload.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | final class Payload extends BasePayload { 23 | public string $path; 24 | 25 | /** 26 | * Get description for this plugin 27 | * @return string 28 | */ 29 | public static function getInfo(): string { 30 | return 'Handles empty queries,' 31 | . ' which can occur when trimming comments or dealing with specific SQL' 32 | . ' protocol instructions in comments that are not supported'; 33 | } 34 | 35 | /** 36 | * @param Request $request 37 | * @return static 38 | */ 39 | public static function fromRequest(Request $request): static { 40 | $self = new static(); 41 | // We just need to do something, but actually its' just for PHPstan 42 | $self->path = $request->path; 43 | return $self; 44 | } 45 | 46 | /** 47 | * @param Request $request 48 | * @return bool 49 | */ 50 | public static function hasMatch(Request $request): bool { 51 | $payload = strtolower($request->payload); 52 | if ($request->payload === '' 53 | && $request->endpointBundle !== Endpoint::Metrics 54 | && $request->endpointBundle !== Endpoint::Bulk 55 | && $request->endpointBundle !== Endpoint::Elastic) { 56 | return true; 57 | } 58 | if ($request->command === 'set') { 59 | $setPatterns = [ 60 | 'sql_quote_show_create', 61 | '@saved_cs_client', 62 | '@@session', 63 | 'character_set_client', 64 | 'session character_set_results', 65 | 'session transaction', 66 | 'sql_select_limit', 67 | ]; 68 | foreach ($setPatterns as $pattern) { 69 | if (stripos($payload, $pattern) === 4) { 70 | return true; 71 | } 72 | } 73 | } 74 | 75 | return match ($request->command) { 76 | 'create' => stripos($payload, 'create database') === 0, 77 | 'lock' => stripos($payload, 'lock tables') === 0, 78 | 'unlock' => stripos($payload, 'unlock tables') === 0, 79 | default => false, 80 | }; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/CatHandler.php: -------------------------------------------------------------------------------- 1 | path); 45 | if (!isset($pathParts[1], $pathParts[2]) 46 | && !in_array($pathParts[1], self::CAT_ENTITIES) && !str_ends_with($pathParts[1], '*')) { 47 | throw new \Exception('Cannot parse request'); 48 | } 49 | $entityTable = "_{$pathParts[1]}"; 50 | $entityNamePattern = $pathParts[2]; 51 | 52 | $query = "SELECT * FROM {$entityTable} WHERE MATCH('{$entityNamePattern}')"; 53 | /** @var array{0:array{data?:array}} $queryResult */ 54 | $queryResult = $manticoreClient->sendRequest($query)->getResult(); 55 | if (!isset($queryResult[0]['data']) || !$queryResult[0]['data']) { 56 | return TaskResult::raw([]); 57 | } 58 | 59 | $catInfo = []; 60 | foreach ($queryResult[0]['data'] as $entityInfo) { 61 | $catInfo[] = [ 62 | 'name' => $entityInfo['name'], 63 | 'order' => 0, 64 | 'index_patterns' => simdjson_decode($entityInfo['patterns'], true), 65 | ] + simdjson_decode($entityInfo['content'], true); 66 | } 67 | 68 | return TaskResult::raw($catInfo); 69 | }; 70 | 71 | return Task::create( 72 | $taskFn, [$this->payload, $this->manticoreClient] 73 | )->run(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/ClusterKibanaHandler.php: -------------------------------------------------------------------------------- 1 | payload->path); 42 | } 43 | 44 | /** 45 | * @return array 46 | */ 47 | public function getProps(): array { 48 | return []; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/CountInfoKibanaHandler.php: -------------------------------------------------------------------------------- 1 | [ 45 | 'failed' => 0, 46 | 'skipped' => 0, 47 | 'successful' => 1, 48 | 'total' => 1, 49 | ], 50 | 'count' => 0, 51 | ] 52 | ); 53 | }; 54 | 55 | return Task::create($taskFn)->run(); 56 | } 57 | 58 | /** 59 | * @return array 60 | */ 61 | public function getProps(): array { 62 | return []; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/FindEntityHandler.php: -------------------------------------------------------------------------------- 1 | path, $manticoreClient); 42 | 43 | $query = 'SELECT _source FROM `' . self::ENTITY_TABLE 44 | . "` WHERE _id='{$entityId}' AND _index='{$entityIndex}'"; 45 | /** @var array{error?:string,0:array{data?:array}} $queryResult */ 46 | $queryResult = $manticoreClient->sendRequest($query)->getResult(); 47 | if (isset($queryResult['error']) || !isset($queryResult[0]['data']) || !$queryResult[0]['data']) { 48 | $resp = [ 49 | '_id' => $entityId, 50 | '_index' => $entityIndex, 51 | '_type' => '_doc', 52 | 'found' => false, 53 | ]; 54 | } else { 55 | $resp = [ 56 | '_id' => $entityId, 57 | '_index' => $entityIndex, 58 | '_primary_term' => 1, 59 | '_seq_no' => 0, 60 | '_source' => simdjson_decode($queryResult[0]['data'][0]['_source'], true), 61 | '_type' => '_doc', 62 | '_version' => 1, 63 | 'found' => true, 64 | ]; 65 | } 66 | 67 | return TaskResult::raw($resp); 68 | }; 69 | 70 | return Task::create( 71 | $taskFn, [$this->payload, $this->manticoreClient] 72 | )->run(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/GetAliasesHandler.php: -------------------------------------------------------------------------------- 1 | }} $queryResult */ 43 | $queryResult = $manticoreClient->sendRequest($query)->getResult(); 44 | if (!isset($queryResult[0])) { 45 | return TaskResult::raw([]); 46 | } 47 | 48 | $aliasInfo = self::get($payload->table, $manticoreClient); 49 | return TaskResult::raw($aliasInfo); 50 | }; 51 | 52 | return Task::create( 53 | $taskFn, [$this->payload, $this->manticoreClient] 54 | )->run(); 55 | } 56 | 57 | /** 58 | * 59 | * @param string $indexAlias 60 | * @param HttpClient $manticoreClient 61 | * @return array 62 | */ 63 | public static function get(string $indexAlias, HTTPClient $manticoreClient): array { 64 | $query = 'SELECT index FROM ' . parent::ALIAS_TABLE . " WHERE alias='{$indexAlias}'"; 65 | /** @var array{0:array{data?:array}} $queryResult */ 66 | $queryResult = $manticoreClient->sendRequest($query)->getResult(); 67 | if (!isset($queryResult[0]['data']) || !$queryResult[0]['data']) { 68 | return []; 69 | } 70 | $aliasInfo = []; 71 | foreach ($queryResult[0]['data'] as $dataRow) { 72 | $aliasInfo[$dataRow['index']] = [ 73 | 'aliases' => [ 74 | $indexAlias => [], 75 | ], 76 | ]; 77 | } 78 | return $aliasInfo; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/InitKibanaHandler.php: -------------------------------------------------------------------------------- 1 | path; 42 | $query = 'SELECT _id, _index, _source FROM `' 43 | . self::ENTITY_TABLE . "` WHERE _index_alias='{$alias}' AND _type='settings'"; 44 | /** @var array{error?:string,0:array{data?:array}} $queryResult */ 45 | $queryResult = $manticoreClient->sendRequest($query)->getResult(); 46 | if (isset($queryResult['error']) || !isset($queryResult[0]['data']) || !$queryResult[0]['data']) { 47 | $resp = [ 48 | 'error' => [ 49 | 'index' => $alias, 50 | 'index_uuid' => '_na_', 51 | 'reason' => "no such index [{$alias}]", 52 | 'resource.id' => $alias, 53 | 'resource.type' => 'index_or_alias', 54 | 'root_cause' => [ 55 | [ 56 | 'index' => $alias, 57 | 'index_uuid' => '_na_', 58 | 'reason' => "no such index [{$alias}]", 59 | 'resource.id' => $alias, 60 | 'resource.type' => 'index_or_alias', 61 | 'type' => 'index_not_found_exception', 62 | ], 63 | ], 64 | 'type' => 'index_not_found_exception', 65 | ], 66 | 'status' => 404, 67 | ]; 68 | } else { 69 | $resp = []; 70 | foreach ($queryResult[0]['data'] as $entity) { 71 | $resp[$entity['_index']] = [ 72 | 'aliases' => [ 73 | $alias => [], 74 | ], 75 | ] + simdjson_decode($entity['_source'], true); 76 | } 77 | } 78 | 79 | return TaskResult::raw($resp); 80 | }; 81 | 82 | return Task::create( 83 | $taskFn, [$this->payload, $this->manticoreClient] 84 | )->run(); 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/InvalidSourceKibanaHandler.php: -------------------------------------------------------------------------------- 1 | body, true); 44 | if (!is_array($request)) { 45 | throw new Exception("Invalid request passed: {$payload->body}"); 46 | } 47 | $request['_source'] = []; 48 | $request['table'] = $payload->table; 49 | if (isset($request['script_fields'])) { 50 | unset($request['script_fields']); 51 | } 52 | $isGetSingleDocQuery = isset($request['query']) && is_array($request['query']) 53 | && isset($request['query']['ids']) && is_array($request['query']['ids']) 54 | && isset($request['query']['ids']['values']) && is_array($request['query']['ids']['values']); 55 | if ($isGetSingleDocQuery) { 56 | $ids = array_map(fn($id) => (int)$id, $request['query']['ids']['values']); 57 | $request['query']['in'] = [ 58 | 'id' => $ids, 59 | ]; 60 | } 61 | $query = json_encode($request); 62 | /** @var array{error?:string} $queryResult */ 63 | $queryResult = $manticoreClient->sendRequest((string)$query, 'search')->getResult(); 64 | 65 | return TaskResult::raw($queryResult); 66 | }; 67 | 68 | return Task::create( 69 | $taskFn, [$this->payload, $this->manticoreClient] 70 | )->run(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/KibanaSearch/Logic/Request/Aliasing.php: -------------------------------------------------------------------------------- 1 | $aliasedNodes 29 | * @param array $fieldNames 30 | */ 31 | public function __construct(private array $aliasedNodes, private array $fieldNames) { 32 | } 33 | 34 | /** 35 | * @return static 36 | */ 37 | public function apply(): static { 38 | foreach ($this->aliasedNodes as $node) { 39 | if ($node->getFieldAlias()) { 40 | continue; 41 | } 42 | $alias = $this->generateAlias(); 43 | $node->setFieldAlias($alias); 44 | } 45 | 46 | return $this; 47 | } 48 | 49 | /** 50 | * @return string 51 | */ 52 | public function generateAlias(): string { 53 | $alias = static::ALIAS_PREFIX . ++$this->aliasCount; 54 | while (in_array($alias, $this->fieldNames)) { 55 | $alias .= '_'; 56 | } 57 | return $alias; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/KibanaSearch/Logic/Request/Executor.php: -------------------------------------------------------------------------------- 1 | $logics */ 25 | protected array $logics = []; 26 | 27 | /** 28 | * @param Factory $logicFactory 29 | */ 30 | public function __construct(protected Factory $logicFactory) { 31 | } 32 | 33 | /** 34 | * @return static 35 | */ 36 | public function init(): static { 37 | $this->logics = array_map( 38 | fn ($logicName) => $this->logicFactory->create($logicName), 39 | static::LOGIC_NAMES 40 | ); 41 | 42 | return $this; 43 | } 44 | 45 | /** 46 | * @return bool 47 | */ 48 | public function execute(): bool { 49 | foreach ($this->logics as $logic) { 50 | $logic->apply(); 51 | if (!($logic instanceof FailableLogicInterface)) { 52 | continue; 53 | } 54 | if ($logic->isFailed()) { 55 | return false; 56 | } 57 | } 58 | return true; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/KibanaSearch/Logic/Request/Interfaces/FailableLogicInterface.php: -------------------------------------------------------------------------------- 1 | > $responseRows */ 23 | protected array $responseRows = []; 24 | 25 | /** 26 | * @param array> $responseRows 27 | * @return static 28 | */ 29 | public function setResponseRows(array $responseRows): static { 30 | $this->responseRows = $responseRows; 31 | return $this; 32 | } 33 | 34 | /** 35 | * @return array> 36 | */ 37 | public function getResponseRows(): array { 38 | return $this->responseRows; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/KibanaSearch/Logic/Response/ConcurrentFilterProcessing/FilterSet.php: -------------------------------------------------------------------------------- 1 | $fields */ 17 | private array $fields = []; 18 | 19 | /** 20 | * @param string $field 21 | * @return self 22 | */ 23 | public function addField(string $field): self { 24 | $this->fields[] = $field; 25 | return $this; 26 | } 27 | 28 | /** 29 | * @return array 30 | */ 31 | public function getFields(): array { 32 | return $this->fields; 33 | } 34 | 35 | /** 36 | * @param array $row 37 | * @return array>|false 38 | */ 39 | public function check(array $row): array|false { 40 | $activeFilters = array_filter( 41 | $this->fields, 42 | // @phpstan-ignore-next-line 43 | fn ($filterField) => $row[$filterField] 44 | ); 45 | if (sizeof($activeFilters) < 2) { 46 | return false; 47 | } 48 | 49 | return self::convertRowToSingleFilterOnes($row, $activeFilters); 50 | } 51 | 52 | /** 53 | * @param array $row 54 | * @param array $activeFilters 55 | * @return array> 56 | */ 57 | private static function convertRowToSingleFilterOnes(array $row, array $activeFilters): array { 58 | $addRows = []; 59 | foreach ($activeFilters as $j => $filterField) { 60 | $newRow = $row; 61 | foreach ($activeFilters as $k => $filterField) { 62 | if ($j === $k) { 63 | continue; 64 | } 65 | $newRow[$filterField] = 0; 66 | } 67 | $addRows[] = $newRow; 68 | } 69 | 70 | return $addRows; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/KibanaSearch/Logic/Response/DisabledMetricAdding.php: -------------------------------------------------------------------------------- 1 | $disabledMetricNodes */ 24 | protected array $disabledMetricNodes = []; 25 | 26 | /** 27 | * @param array $metricNodes 28 | */ 29 | public function __construct(protected array $metricNodes) { 30 | } 31 | 32 | /** 33 | * @return bool 34 | */ 35 | public function isAvailable(): bool { 36 | $this->disabledMetricNodes = array_filter( 37 | $this->metricNodes, 38 | fn ($node) => $node->isDisabled() 39 | ); 40 | return !!sizeof($this->disabledMetricNodes); 41 | } 42 | 43 | /** 44 | * @return static 45 | */ 46 | public function apply(): static { 47 | $metricFields = array_map( 48 | fn ($node) => $node->getFieldAlias() ?: $node->getField(), 49 | $this->disabledMetricNodes 50 | ); 51 | foreach (array_keys($this->responseRows) as $i) { 52 | foreach ($metricFields as $field) { 53 | $this->responseRows[$i][$field] = ''; 54 | } 55 | } 56 | 57 | return $this; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/KibanaSearch/Logic/Response/Executor.php: -------------------------------------------------------------------------------- 1 | $logics */ 30 | protected array $logics; 31 | 32 | /** @var array> $responseRows */ 33 | protected array $responseRows = []; 34 | 35 | /** 36 | * @param array> $responseRows 37 | * @return static 38 | */ 39 | public function setResponseRows(array $responseRows): static { 40 | $this->responseRows = $responseRows; 41 | return $this; 42 | } 43 | 44 | /** 45 | * @return array> 46 | */ 47 | public function getResponseRows(): array { 48 | return $this->responseRows; 49 | } 50 | 51 | /** 52 | * @return bool 53 | */ 54 | public function execute(): bool { 55 | $availableLogics = array_filter( 56 | $this->logics, 57 | fn ($logic) => $logic->isAvailable() 58 | ); 59 | 60 | foreach ($availableLogics as $logic) { 61 | $this->responseRows = $logic 62 | ->setResponseRows($this->responseRows) 63 | ->apply() 64 | ->getResponseRows(); 65 | } 66 | 67 | return true; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/KibanaSearch/Logic/Response/Interfaces/ResponseLogicInterface.php: -------------------------------------------------------------------------------- 1 | > $responseRows 23 | * @return static 24 | */ 25 | public function setResponseRows(array $responseRows): static; 26 | 27 | /** 28 | * @return array> 29 | */ 30 | public function getResponseRows(): array; 31 | 32 | /** @return bool */ 33 | public function isAvailable(): bool; 34 | } 35 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/KibanaSearch/Logic/Response/Sorting/Metric/CalculatorFactory.php: -------------------------------------------------------------------------------- 1 | countField); 33 | } 34 | 35 | /** 36 | * @return CountCalculator 37 | */ 38 | public function create(): CountCalculator { 39 | return new CountCalculator(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/KibanaSearch/Logic/Response/Sorting/Metric/CountCalculator.php: -------------------------------------------------------------------------------- 1 | > $sortRows 21 | * @param string $sortField 22 | * @return int|float|false 23 | */ 24 | public function calc(array $sortRows, string $sortField): int|float|false { 25 | return array_sum( 26 | array_column($sortRows, $sortField) 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/KibanaSearch/Logic/Response/Sorting/Metric/MetricCalculatorInterface.php: -------------------------------------------------------------------------------- 1 | > $sortRows 21 | * @param string $sortField 22 | * @return int|float|false 23 | */ 24 | public function calc(array $sortRows, string $sortField): int|float|false; 25 | } 26 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/KibanaSearch/Logic/Response/Sorting/Metric/MetricUpdaterInterface.php: -------------------------------------------------------------------------------- 1 | > 29 | */ 30 | public function getUpdates(): array; 31 | } 32 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/KibanaSearch/Logic/Response/Sorting/Metric/Operation.php: -------------------------------------------------------------------------------- 1 | field; 29 | } 30 | 31 | /** 32 | * @param SphinxQLRequest $request 33 | * @return static 34 | */ 35 | public function setRequest(SphinxQLRequest $request): static { 36 | parent::setRequest($request); 37 | $this->countField = $request->getCountField(); 38 | return $this; 39 | } 40 | 41 | /** 42 | * @param array $responseNode 43 | * @param array $extraData 44 | * @return void 45 | */ 46 | protected function makeResponseBucketsIfNotExist(array &$responseNode, array $extraData = []): void { 47 | if (array_key_exists($this->key, $responseNode) 48 | && (is_array($responseNode[$this->key]) && array_key_exists('buckets', $responseNode[$this->key])) 49 | ) { 50 | return; 51 | } 52 | if (!array_key_exists($this->key, $responseNode)) { 53 | $responseNode[$this->key] = []; 54 | } 55 | /** @var array $subNode */ 56 | $subNode = &$responseNode[$this->key]; 57 | $subNode['buckets'] = []; 58 | if (!$extraData) { 59 | return; 60 | } 61 | $responseNode[$this->key] += $extraData; 62 | } 63 | 64 | /** 65 | * @param array> $buckets 66 | * @param string $key 67 | * @param mixed $val 68 | * @return int 69 | */ 70 | public function findBucket(array $buckets, string $key, mixed $val): int { 71 | foreach ($buckets as $i => $bucket) { 72 | if (array_key_exists($key, $bucket) && $bucket[$key] === $val) { 73 | return $i; 74 | } 75 | } 76 | return -1; 77 | } 78 | 79 | /** 80 | * @param array $responseNode 81 | * @param array $dataRow 82 | * @param string $nextNodeKey 83 | * @return array|false 84 | */ 85 | abstract public function fillInResponse(array &$responseNode, array $dataRow, string $nextNodeKey): array|false; 86 | } 87 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/KibanaSearch/RequestNode/BaseNode.php: -------------------------------------------------------------------------------- 1 | key; 31 | } 32 | 33 | /** 34 | * @param SphinxQLRequest $request 35 | * @return static 36 | */ 37 | public function setRequest(SphinxQLRequest $request): static { 38 | $this->request = $request; 39 | return $this; 40 | } 41 | 42 | /** @return void */ 43 | public function disable(): void { 44 | $this->isDisabled = true; 45 | } 46 | 47 | /** 48 | * @return bool 49 | */ 50 | public function isDisabled(): bool { 51 | return $this->isDisabled; 52 | } 53 | 54 | /** 55 | * @return void 56 | */ 57 | abstract public function fillInRequest(): void; 58 | } 59 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/KibanaSearch/RequestNode/BaseRange.php: -------------------------------------------------------------------------------- 1 | $ranges */ 26 | protected array $ranges; 27 | 28 | /** 29 | * @return int 30 | */ 31 | public function getRangeCount(): int { 32 | return sizeof($this->ranges); 33 | } 34 | 35 | /** 36 | * @return void 37 | */ 38 | protected function makeFieldExpr(): void { 39 | $rangeExprs = []; 40 | foreach ($this->ranges as $range) { 41 | $rangeExpr = ''; 42 | if ($range['from']) { 43 | $rangeExpr = "range_from={$range['from']}"; 44 | } 45 | if ($range['to']) { 46 | $rangeExpr .= ($rangeExpr ? ',' : '') . "range_to={$range['to']}"; 47 | } 48 | if (in_array($rangeExpr, $rangeExprs)) { 49 | // There's no need to send the same range multiple times 50 | continue; 51 | } 52 | $rangeExprs[] = "{{$rangeExpr}}"; 53 | } 54 | $this->fieldExpr = static::EXPR_FUNC . "({$this->argField}," . implode(',', $rangeExprs) . ')'; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/KibanaSearch/RequestNode/ExprNode.php: -------------------------------------------------------------------------------- 1 | fieldAlias; 33 | } 34 | 35 | /** 36 | * @param string $alias 37 | * @return void 38 | */ 39 | public function setFieldAlias(string $alias): void { 40 | $this->fieldAlias = $alias; 41 | } 42 | 43 | /** 44 | * @return string 45 | */ 46 | public function getField(): string { 47 | if (!$this->fieldExpr) { 48 | $this->makeFieldExpr(); 49 | } 50 | return $this->fieldExpr; 51 | } 52 | 53 | /** 54 | * @return string 55 | */ 56 | public function getArgField(): string { 57 | return $this->argField; 58 | } 59 | 60 | /** 61 | * @return void 62 | */ 63 | public function fillInRequest(): void { 64 | if (!$this->fieldExpr) { 65 | $this->makeFieldExpr(); 66 | } 67 | $this->request->addField($this->fieldExpr, $this->fieldAlias); 68 | } 69 | 70 | /** 71 | * @ return void 72 | */ 73 | abstract protected function makeFieldExpr(): void; 74 | } 75 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/KibanaSearch/RequestNode/GroupExprNode.php: -------------------------------------------------------------------------------- 1 | groupField = $this->fieldAlias ?: $this->field; 28 | $this->request->addGroupField($this->groupField); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/KibanaSearch/RequestNode/Helpers/FilterExpression/Factory.php: -------------------------------------------------------------------------------- 1 | $nonAggFields 21 | */ 22 | public function __construct(private array $nonAggFields) { 23 | } 24 | 25 | /** 26 | * @return FilterExpression 27 | */ 28 | public function create() { 29 | return new FilterExpression($this->nonAggFields); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/KibanaSearch/RequestNode/Helpers/TimeZoneExpression.php: -------------------------------------------------------------------------------- 1 | getOffset(new \DateTime) / 3600; 28 | 29 | return '+' . ($offset < 10 ? '0' : '') . $offset . ':00'; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/KibanaSearch/RequestNode/Interfaces/AggNodeInterface.php: -------------------------------------------------------------------------------- 1 | $responseNode 25 | * @param array $dataRow 26 | * @param string $nextNodeKey 27 | * @return array|false 28 | */ 29 | public function fillInResponse(array &$responseNode, array $dataRow, string $nextNodeKey): array|false; 30 | } 31 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/KibanaSearch/RequestNode/Interfaces/AliasedNodeInterface.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | public function getFilter(): array; 28 | } 29 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/KibanaSearch/RequestNode/Metric.php: -------------------------------------------------------------------------------- 1 | name = $this->func = $nodeName; 39 | } 40 | 41 | /** 42 | * @return string 43 | */ 44 | public function getName(): string { 45 | return $this->name; 46 | } 47 | 48 | /** 49 | * @return string 50 | */ 51 | public function getFunc(): string { 52 | return $this->func; 53 | } 54 | 55 | /** 56 | * @return string 57 | */ 58 | public function getArgField(): string { 59 | return $this->argField; 60 | } 61 | 62 | /** 63 | * @param array $responseNode 64 | * @param array $dataRow 65 | * @param string $nextNodeKey 66 | * @return array|false 67 | */ 68 | public function fillInResponse(array &$responseNode, array $dataRow, string $nextNodeKey): array|false { 69 | /** @var array{value:string} $subNode */ 70 | $subNode = &$responseNode[$this->key]; 71 | if (array_key_exists($this->key, $dataRow)) { 72 | $subNode['value'] = $dataRow[$this->key]; 73 | } else { 74 | $dataField = $this->fieldAlias ?: $this->getField(); 75 | if (array_key_exists($dataField, $dataRow)) { 76 | $subNode['value'] = $dataRow[$dataField]; 77 | } 78 | } 79 | return $nextNodeKey === '' ? [$nextNodeKey] : []; 80 | } 81 | 82 | /** 83 | * @ return void 84 | */ 85 | protected function makeFieldExpr(): void { 86 | $this->fieldExpr = "{$this->func}({$this->argField})"; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/KibanaSearch/TableFieldInfo.php: -------------------------------------------------------------------------------- 1 | >> $fieldPerTableInfo */ 24 | protected $fieldPerTableInfo = []; 25 | /** @var array> $fieldInfo */ 26 | protected array $fieldInfo = []; 27 | /** @var array $tables */ 28 | protected array $tables = []; 29 | 30 | /** 31 | * @param string $requestTable 32 | * @param Client $manticoreClient 33 | */ 34 | public function __construct(protected string $requestTable, protected Client $manticoreClient) { 35 | $this->load(); 36 | } 37 | 38 | /** 39 | * @return array 40 | */ 41 | public function getTables(): array { 42 | return $this->tables; 43 | } 44 | 45 | /** 46 | * @return array> 47 | */ 48 | public function get(): array { 49 | return $this->fieldInfo; 50 | } 51 | 52 | /** 53 | * @param string $table 54 | * @return array 55 | */ 56 | public function getFieldNamesByTable(string $table): array { 57 | if (sizeof($this->tables) < 2) { 58 | return array_keys($this->fieldInfo); 59 | } 60 | if (!array_key_exists($table, $this->fieldPerTableInfo)) { 61 | $this->load($table); 62 | } 63 | return array_keys($this->fieldPerTableInfo[$table]); 64 | } 65 | 66 | /** 67 | * @return array 68 | */ 69 | public function getFieldNames(): array { 70 | return array_keys($this->fieldInfo); 71 | } 72 | 73 | /** 74 | * @return void 75 | */ 76 | protected function load(string $table = ''): void { 77 | $requestTable = $table ?: $this->requestTable; 78 | /** @var array{ 79 | * fields:array>, 80 | * indices:array 81 | * } $requestTableInfo 82 | */ 83 | $requestTableInfo = $this->manticoreClient->sendRequest( 84 | '{"fields":"*"}', 85 | $requestTable . static::REQUEST_TABLE_INFO_ENDPOINT, 86 | true 87 | )->getResult(); 88 | 89 | if ($table) { 90 | $this->fieldPerTableInfo[$table] = $requestTableInfo['fields']; 91 | } else { 92 | $this->fieldInfo = $requestTableInfo['fields']; 93 | $this->tables = $requestTableInfo['indices']; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/LicenseHandler.php: -------------------------------------------------------------------------------- 1 | [ 45 | 'status' => 'active', 46 | 'uid' => 'no_license', 47 | 'type' => 'basic', 48 | 'issue_date' => '2023-01-01T00:00:000.000Z', 49 | 'issue_date_in_millis' => 0, 50 | 'max_nodes' => 1000, 51 | 'issued_to' => 'docker-cluster', 52 | 'issuer' => 'elasticsearch', 53 | 'start_date_in_millis' => -1, 54 | ], 55 | ] 56 | ); 57 | }; 58 | 59 | return Task::create($taskFn)->run(); 60 | } 61 | 62 | /** 63 | * @return array 64 | */ 65 | public function getProps(): array { 66 | return []; 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/ManagerSettingsKibanaHandler.php: -------------------------------------------------------------------------------- 1 | payload->path); 42 | } 43 | 44 | /** 45 | * @return array 46 | */ 47 | public function getProps(): array { 48 | return []; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/MetricKibanaHandler.php: -------------------------------------------------------------------------------- 1 | run(); 49 | } 50 | 51 | /** 52 | * @return array 53 | */ 54 | public function getProps(): array { 55 | return []; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/QueryMapLoaderTrait.php: -------------------------------------------------------------------------------- 1 | > $queryMap */ 21 | protected static $queryMap = []; 22 | 23 | /** 24 | * @param string $mapName 25 | * @return void 26 | */ 27 | protected static function initQueryMap(string $mapName): void { 28 | if (isset(static::$queryMap[$mapName])) { 29 | return; 30 | } 31 | $queryMapFilePattern = __DIR__ . '/QueryMap/%MAP_NAME%.php'; 32 | /** @var array $queryMap */ 33 | $queryMap = include (string)str_replace('%MAP_NAME%', $mapName, $queryMapFilePattern); 34 | static::$queryMap[$mapName] = $queryMap; 35 | } 36 | 37 | /** 38 | * @param string $query 39 | * @param string $mapName 40 | * @param ?\Closure $preprocessor 41 | * @return Task 42 | * @throws RuntimeException 43 | */ 44 | protected static function getResponseByQuery(string $mapName, string $query, ?\Closure $preprocessor = null): Task { 45 | if (!isset(self::$queryMap[$mapName])) { 46 | throw new \Exception("Unknown error on $mapName query map load"); 47 | } 48 | if (!isset(self::$queryMap[$mapName][$query])) { 49 | throw new \Exception("Unknown request path passed: $query"); 50 | } 51 | 52 | /** @var array $resp */ 53 | $resp = self::$queryMap[$mapName][$query]; 54 | if ($preprocessor !== null) { 55 | $preprocessor($resp); 56 | } 57 | $taskFn = static function (array $resp): TaskResult { 58 | return TaskResult::raw($resp); 59 | }; 60 | 61 | return Task::create($taskFn, [$resp])->run(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/SettingsKibanaHandler.php: -------------------------------------------------------------------------------- 1 | payload->path); 42 | } 43 | 44 | /** 45 | * @return array 46 | */ 47 | public function getProps(): array { 48 | return []; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/Plugin/EmulateElastic/TelemetryKibanaHandler.php: -------------------------------------------------------------------------------- 1 | 1, 44 | '_seq_no' => 0, 45 | 'updated' => 1, 46 | '_id' => 'telemetry:telemetry', 47 | '_index' => '.kibana', 48 | '_source' => [ 49 | 'references' => [], 50 | 'telemetry' => [ 51 | 'userHasSeenNotice' => true, 52 | ], 53 | 'type' => 'telemetry', 54 | 'updated_at' => '2024-05-28T11:23:42.444Z', 55 | ], 56 | '_type' => '_doc', 57 | '_version' => 1, 58 | 'found' => true, 59 | ] 60 | ); 61 | }; 62 | 63 | return Task::create($taskFn)->run(); 64 | } 65 | 66 | /** 67 | * @return array 68 | */ 69 | public function getProps(): array { 70 | return []; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/Plugin/Insert/Error/AutoSchemaDisabledError.php: -------------------------------------------------------------------------------- 1 | $cols 22 | */ 23 | protected array $cols = []; 24 | /** 25 | * @var array $colTypes 26 | */ 27 | protected array $colTypes = []; 28 | /** 29 | * @var array $rows 30 | */ 31 | protected array $rows = []; 32 | /** 33 | * @var string $name 34 | */ 35 | protected string $error = ''; 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/Plugin/Insert/QueryParser/Datalim.php: -------------------------------------------------------------------------------- 1 | ,colTypes:array} 20 | */ 21 | public function parse(string $query): array; 22 | 23 | /** 24 | * Checking for unescaped characters. Just as a test feature so far 25 | * 26 | * @param string|array $row 27 | * @param class-string $errorHandler 28 | * @return void 29 | */ 30 | public static function checkUnescapedChars(mixed $row, string $errorHandler): void; 31 | 32 | /** 33 | * @param callable $checker 34 | * @param array $rowVals 35 | * @param array &$types 36 | * @param array $cols 37 | * @param class-string $errorHandler 38 | * @return void 39 | */ 40 | public static function checkColTypesError( 41 | callable $checker, 42 | array $rowVals, 43 | array &$types, 44 | array $cols, 45 | string $errorHandler 46 | ): void; 47 | } 48 | -------------------------------------------------------------------------------- /src/Plugin/Insert/QueryParser/JSONParser.php: -------------------------------------------------------------------------------- 1 | cols = $this->colTypes = []; 30 | $isNdJson = !Struct::isValid($query); 31 | if ($isNdJson) { 32 | // checking if query has ndjson format 33 | $queries = static::parseNdJSON($query); 34 | foreach ($queries as $query) { 35 | $struct = Struct::fromJson($query); 36 | $row = $struct->toArray(); 37 | if (!$row || !is_array($row)) { 38 | throw new QueryParseError('Invalid JSON in query'); 39 | } 40 | $this->isNdJSON = true; 41 | $this->parseJSONRow($row); 42 | } 43 | } else { 44 | $struct = Struct::fromJson($query); 45 | $row = $struct->toArray(); 46 | $this->parseJSONRow($row); 47 | } 48 | 49 | if ($this->error !== '') { 50 | throw new QueryParseError($this->error); 51 | } 52 | return ['name' => $this->name]; 53 | } 54 | 55 | /** 56 | * @param string $query 57 | * @return Iterable 58 | */ 59 | public static function parseNdJSON($query): Iterable { 60 | do { 61 | $eolPos = strpos($query, PHP_EOL); 62 | if ($eolPos === false) { 63 | $eolPos = strlen($query); 64 | } 65 | $row = substr($query, 0, $eolPos); 66 | if ($row !== '') { 67 | yield $row; 68 | } 69 | $query = substr($query, $eolPos + 1); 70 | } while (strlen($query) > 0); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/Plugin/Insert/QueryParser/JSONParserInterface.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public static function parseNdJSON(string $query): Iterable; 20 | /** 21 | * @param array $query 22 | * @return array 23 | */ 24 | public function parseJSONRow(array $query): array; 25 | } 26 | -------------------------------------------------------------------------------- /src/Plugin/Insert/QueryParser/Loader.php: -------------------------------------------------------------------------------- 1 | value !== $requestPath); 28 | } 29 | 30 | /** 31 | * @param string $reqPath 32 | * @param ManticoreEndpoint $reqEndpointBundle 33 | * @return InsertQueryParserInterface 34 | */ 35 | public static function getInsertQueryParser( 36 | string $reqPath, 37 | ManticoreEndpoint $reqEndpointBundle 38 | ): InsertQueryParserInterface { 39 | // Resolve the possible ambiguity with Manticore query format as it may not correspond to request format 40 | $reqFormat = match ($reqEndpointBundle) { 41 | ManticoreEndpoint::Cli, ManticoreEndpoint::CliJson, ManticoreEndpoint::Sql => RequestFormat::SQL, 42 | ManticoreEndpoint::Insert, ManticoreEndpoint::Replace, ManticoreEndpoint::Bulk => RequestFormat::JSON, 43 | default => throw new ParserLoadError("Unsupported endpoint bundle '{$reqEndpointBundle->value}' passed"), 44 | }; 45 | $parserClass = match ($reqFormat) { 46 | RequestFormat::SQL => 'SQLInsertParser', 47 | RequestFormat::JSON => self::isElasticLikeRequest($reqPath, $reqEndpointBundle) 48 | ? 'ElasticJSONInsertParser' 49 | : 'JSONInsertParser', 50 | }; 51 | $parserClassFull = __NAMESPACE__ . '\\' . $parserClass; 52 | $parser = ($parserClassFull === __NAMESPACE__ . '\ElasticJSONInsertParser') 53 | ? new $parserClassFull($reqPath) 54 | : new $parserClassFull(); 55 | if ($parser instanceof InsertQueryParserInterface) { 56 | return $parser; 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/Plugin/Insert/QueryParser/QueryParserInterface.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public function parse(string $query): array; 20 | } 21 | -------------------------------------------------------------------------------- /src/Plugin/Metrics/Payload.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | final class Payload extends BasePayload 23 | { 24 | public string $path; 25 | 26 | public string $table; 27 | 28 | 29 | /** 30 | * Get description for this plugin 31 | * @return string 32 | */ 33 | public static function getInfo(): string { 34 | return 'Returns Prometheus metrics'; 35 | } 36 | 37 | /** 38 | * @param Request $request 39 | * @return static 40 | */ 41 | public static function fromRequest(Request $request): static { 42 | $self = new static(); 43 | $self->path = $request->path; 44 | return $self; 45 | } 46 | 47 | /** 48 | * @param Request $request 49 | * @return bool 50 | */ 51 | public static function hasMatch(Request $request): bool { 52 | return $request->endpointBundle->value === 'metrics'; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Plugin/ModifyTable/Handler.php: -------------------------------------------------------------------------------- 1 | type} table {$payload->table} {$payload->structure} {$payload->extra}"; 38 | $resp = $client->sendRequest($q, disableAgentHeader: true); 39 | return TaskResult::fromResponse($resp); 40 | }; 41 | 42 | $task = Task::create( 43 | $taskFn, 44 | [$this->payload, $this->manticoreClient] 45 | ); 46 | 47 | return $task->run(); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/Plugin/Plugin/ActionType.php: -------------------------------------------------------------------------------- 1 | $payload 29 | * @return void 30 | */ 31 | public function __construct(public Payload $payload) { 32 | } 33 | 34 | /** 35 | * Process the request 36 | * @return Task 37 | */ 38 | public function run(): Task { 39 | 40 | $name = $this->getName($this->payload); 41 | $tableName = $this->getTableName(); 42 | 43 | /** 44 | * @param string $name 45 | * @param string $tableName 46 | * @return TaskResult 47 | */ 48 | $taskFn = function (string $name, string $tableName): TaskResult { 49 | $manticoreClient = $this->manticoreClient; 50 | if (!$manticoreClient->hasTable($tableName)) { 51 | return TaskResult::none(); 52 | } 53 | 54 | return TaskResult::withTotal($this->processDrop($name, $tableName)); 55 | }; 56 | 57 | return Task::create( 58 | $taskFn, 59 | [$name, $tableName] 60 | )->run(); 61 | } 62 | 63 | /** 64 | * @param string $name 65 | * @param string $tableName 66 | * @return int 67 | */ 68 | abstract protected function processDrop(string $name, string $tableName): int; 69 | 70 | /** 71 | * @param Payload $payload 72 | * @return string 73 | */ 74 | abstract protected function getName(Payload $payload): string; 75 | 76 | /** 77 | * @return string 78 | */ 79 | abstract protected function getTableName(): string; 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/Plugin/Queue/Handlers/BaseViewHandler.php: -------------------------------------------------------------------------------- 1 | $payload 31 | * @return void 32 | */ 33 | public function __construct(public Payload $payload) { 34 | } 35 | 36 | 37 | /** 38 | * Process the request 39 | * @return Task 40 | */ 41 | public function run(): Task { 42 | 43 | $tableName = $this->getTableName(); 44 | /** 45 | * @param string $tableName 46 | * @param Client $manticoreClient 47 | * @return TaskResult 48 | * @throws ManticoreSearchClientError 49 | */ 50 | $taskFn = static function (string $tableName, Client $manticoreClient): TaskResult { 51 | 52 | 53 | if (!$manticoreClient->hasTable($tableName)) { 54 | return TaskResult::none(); 55 | } 56 | 57 | $sql = /** @lang manticore */ 58 | "SELECT name FROM $tableName GROUP BY name"; 59 | $resp = $manticoreClient->sendRequest($sql); 60 | if ($resp->hasError()) { 61 | throw ManticoreSearchClientError::create((string)$resp->getError()); 62 | } 63 | 64 | return TaskResult::fromResponse($resp); 65 | }; 66 | 67 | return Task::create( 68 | $taskFn, 69 | [$tableName, $this->manticoreClient] 70 | )->run(); 71 | } 72 | 73 | abstract protected function getTableName(): string; 74 | } 75 | -------------------------------------------------------------------------------- /src/Plugin/Queue/Handlers/Source/BaseCreateSourceHandler.php: -------------------------------------------------------------------------------- 1 | $payload 31 | * @return void 32 | */ 33 | public function __construct(public Payload $payload) { 34 | } 35 | 36 | /** 37 | * Process the request 38 | * @return Task 39 | */ 40 | public function run(): Task { 41 | /** 42 | * @param Payload $payload 43 | * @param Client $manticoreClient 44 | * @return TaskResult 45 | * @throws ManticoreSearchClientError 46 | */ 47 | $taskFn = static function (Payload $payload, Client $manticoreClient): TaskResult { 48 | 49 | self::checkAndCreateSource($manticoreClient); 50 | return static::handle($payload, $manticoreClient); 51 | }; 52 | 53 | return Task::create( 54 | $taskFn, 55 | [$this->payload, $this->manticoreClient] 56 | )->run(); 57 | } 58 | 59 | /** 60 | * @throws ManticoreSearchClientError 61 | */ 62 | protected static function checkAndCreateSource(Client $manticoreClient): void { 63 | if ($manticoreClient->hasTable(Payload::SOURCE_TABLE_NAME)) { 64 | return; 65 | } 66 | 67 | $sql = /** @lang ManticoreSearch */ 68 | 'CREATE TABLE ' . Payload::SOURCE_TABLE_NAME . 69 | ' (id bigint, type text, name text attribute indexed, '. 70 | 'full_name text, buffer_table text, attrs json, custom_mapping json, original_query text)'; 71 | 72 | $request = $manticoreClient->sendRequest($sql); 73 | if ($request->hasError()) { 74 | throw ManticoreSearchClientError::create((string)$request->getError()); 75 | } 76 | } 77 | 78 | /** 79 | * @param Payload $payload 80 | * @param Client $manticoreClient 81 | * @return TaskResult 82 | */ 83 | abstract public static function handle(Payload $payload, Client $manticoreClient): TaskResult; 84 | } 85 | -------------------------------------------------------------------------------- /src/Plugin/Queue/Handlers/Source/ViewSourceHandler.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | final class ViewSourceHandler extends BaseViewHandler { 28 | protected function getTableName(): string { 29 | return Payload::SOURCE_TABLE_NAME; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Plugin/Queue/Handlers/View/GetViewHandler.php: -------------------------------------------------------------------------------- 1 | 34 | * }, 35 | * base_expr: string 36 | * } 37 | * } 38 | * }> 39 | */ 40 | final class GetViewHandler extends BaseGetHandler { 41 | 42 | /** 43 | * @param Payload 59 | * }, 60 | * base_expr: string 61 | * } 62 | * } 63 | * }> $payload 64 | * @return string 65 | */ 66 | protected function getName(Payload $payload): string { 67 | $parsedPayload = $payload->model->getPayload(); 68 | return $parsedPayload['SHOW'][2]['no_quotes']['parts'][0]; 69 | } 70 | 71 | protected static function formatResult(string $query): string { 72 | return str_replace("\n", '', $query); 73 | } 74 | 75 | protected function getType(): string { 76 | return 'View'; 77 | } 78 | 79 | protected function getTableName(): string { 80 | return Payload::VIEWS_TABLE_NAME; 81 | } 82 | 83 | protected function getFields(): array { 84 | return ['suspended']; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Plugin/Queue/Handlers/View/ViewViewsHandler.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | final class ViewViewsHandler extends BaseViewHandler { 32 | 33 | 34 | protected function getTableName(): string { 35 | return Payload::VIEWS_TABLE_NAME; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Plugin/Queue/Models/Alter/AlterMaterializedViewModel.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class AlterMaterializedViewModel extends Model { 22 | 23 | public function getHandlerClass(): string { 24 | return 'Handlers\\View\\AlterViewHandler'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Plugin/Queue/Models/Create/CreateMaterializedViewModel.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class CreateMaterializedViewModel extends Model { 22 | 23 | 24 | public function getHandlerClass(): string { 25 | return 'Handlers\\View\\CreateViewHandler'; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Plugin/Queue/Models/Create/CreateSourceModel.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class CreateSourceModel extends Model { 23 | 24 | /** 25 | * @throws GenericError 26 | */ 27 | public function getHandlerClass(): string { 28 | return $this->parseSourceType(); 29 | } 30 | 31 | 32 | /** 33 | * @throws GenericError 34 | */ 35 | public function parseSourceType(): string { 36 | foreach ($this->getPayload()['SOURCE']['options'] as $option) { 37 | if (isset($option['sub_tree'][0]['base_expr']) 38 | && $option['sub_tree'][0]['base_expr'] === 'type') { 39 | return match (SqlQueryParser::removeQuotes($option['sub_tree'][2]['base_expr'])) { 40 | 'kafka' => 'Handlers\\Source\\CreateKafka', 41 | default => throw new GenericError('Cannot find handler for request type: ' . static::class) 42 | }; 43 | } 44 | } 45 | throw new GenericError('Cannot find handler for request type: ' . static::class); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Plugin/Queue/Models/Drop/DropMaterializedViewModel.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class DropMaterializedViewModel extends Model { 21 | 22 | public function getHandlerClass(): string { 23 | return 'Handlers\\View\\DropViewHandler'; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Plugin/Queue/Models/Drop/DropSourceModel.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class DropSourceModel extends Model { 21 | 22 | public function getHandlerClass(): string { 23 | return 'Handlers\\Source\\DropSourceHandler'; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Plugin/Queue/Models/Factories/AlterFactory.php: -------------------------------------------------------------------------------- 1 | |null 24 | */ 25 | public static function create(array $parsedPayload): ?Model { 26 | 27 | $model = null; 28 | 29 | if (self::isAlterMaterializedViewMatch($parsedPayload)) { 30 | $model = new AlterMaterializedViewModel($parsedPayload); 31 | } 32 | 33 | /** @var Model|null $model */ 34 | return $model; 35 | } 36 | 37 | 38 | /** 39 | * Should match ALTER MATERIALIZED VIEW {name} suspended=0; 40 | * 41 | * @param array{ 42 | * ALTER?: array{ 43 | * base_expr: string, 44 | * sub_tree: mixed[] 45 | * }, 46 | * VIEW?: array{ 47 | * base_expr: string, 48 | * name: string, 49 | * no_quotes: array{ 50 | * delim: bool, 51 | * parts: string[] 52 | * }, 53 | * create-def: bool, 54 | * options: array{ 55 | * expr_type: string, 56 | * base_expr: string, 57 | * delim: string, 58 | * sub_tree: array{ 59 | * expr_type: string, 60 | * base_expr: string, 61 | * delim: string, 62 | * sub_tree: array{ 63 | * expr_type: string, 64 | * base_expr: string 65 | * }[] 66 | * }[] 67 | * }[] 68 | * } 69 | * } $parsedPayload 70 | * @return bool 71 | * 72 | */ 73 | private static function isAlterMaterializedViewMatch(array $parsedPayload): bool { 74 | return ( 75 | isset($parsedPayload['ALTER']['base_expr']) && 76 | !empty($parsedPayload['VIEW']['no_quotes']['parts']) && 77 | !empty($parsedPayload['VIEW']['options']) && 78 | strtolower($parsedPayload['ALTER']['base_expr']) === Payload::TYPE_MATERLIALIZED . ' ' . Payload::TYPE_VIEW 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Plugin/Queue/Models/Model.php: -------------------------------------------------------------------------------- 1 | parsedPayload = $parsedPayload; 29 | } 30 | 31 | /** 32 | * @phpstan-return T array 33 | */ 34 | final public function getPayload(): array { 35 | return $this->parsedPayload; 36 | } 37 | 38 | abstract public function getHandlerClass(): string; 39 | } 40 | -------------------------------------------------------------------------------- /src/Plugin/Queue/Models/Show/ShowMaterializedViewModel.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class ShowMaterializedViewModel extends Model { 22 | 23 | public function getHandlerClass(): string { 24 | return 'Handlers\\View\\GetViewHandler'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Plugin/Queue/Models/Show/ShowMaterializedViewsModel.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class ShowMaterializedViewsModel extends Model { 21 | 22 | public function getHandlerClass(): string { 23 | return 'Handlers\\View\\ViewViewsHandler'; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Plugin/Queue/Models/Show/ShowSourceModel.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class ShowSourceModel extends Model { 22 | 23 | public function getHandlerClass(): string { 24 | return 'Handlers\\Source\\GetSourceHandler'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Plugin/Queue/Models/Show/ShowSourcesModel.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class ShowSourcesModel extends Model { 22 | 23 | public function getHandlerClass(): string { 24 | return 'Handlers\\Source\\ViewSourceHandler'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Plugin/Queue/Models/SqlModelsHandler.php: -------------------------------------------------------------------------------- 1 | |null 25 | */ 26 | public static function handle(?array $parsed): ?Model { 27 | 28 | // Order here is important !!! 29 | if (isset($parsed['ALTER'])) { 30 | /** @var Model|null $result */ 31 | $result = AlterFactory::create($parsed); 32 | return $result; 33 | } 34 | 35 | if (isset($parsed['CREATE'])) { 36 | /** @var Model|null $result */ 37 | $result = CreateFactory::create($parsed); 38 | return $result; 39 | } 40 | 41 | if (isset($parsed['DROP'])) { 42 | /** @var Model|null $result */ 43 | $result = DropFactory::create($parsed); 44 | return $result; 45 | } 46 | 47 | if (isset($parsed['SHOW'])) { 48 | /** @var Model|null $result */ 49 | $result = ShowFactory::create($parsed); 50 | return $result; 51 | } 52 | 53 | return null; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Plugin/Queue/Workers/Kafka/Batch.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | private array $batch = []; 23 | 24 | /** 25 | * @var Closure(array): bool $closure 26 | */ 27 | private Closure $callback; 28 | 29 | private int $lastCallTime = 0; 30 | 31 | public function __construct(int $batchSize = 50) { 32 | $this->setBatchSize($batchSize); 33 | } 34 | 35 | public function setBatchSize(int $batchSize): void { 36 | $this->batchSize = $batchSize; 37 | } 38 | 39 | /** 40 | * @param Closure(array): bool $closure 41 | * @return void 42 | */ 43 | public function setCallback(Closure $closure): void { 44 | $this->callback = $closure; 45 | } 46 | 47 | public function checkProcessingTimeout(): bool { 48 | return $this->lastCallTime + 10 < time(); 49 | } 50 | 51 | public function add(mixed $item): bool { 52 | $this->lastCallTime = time(); 53 | $this->batch[] = $item; 54 | 55 | if (sizeof($this->batch) < $this->batchSize) { 56 | return false; 57 | } 58 | return $this->process(); 59 | } 60 | 61 | public function process(): bool { 62 | if (empty($this->batch)) { 63 | return false; 64 | } 65 | $run = call_user_func($this->callback, $this->batch); 66 | $this->batch = []; 67 | return $run; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Plugin/Sharding/DescHandler.php: -------------------------------------------------------------------------------- 1 | payload->table; 38 | // TODO: think about the way to refactor it and remove duplication 39 | $q = "DESC {$table} OPTION force=1"; 40 | $resp = $this->manticoreClient->sendRequest($q); 41 | /** @var array{0:array{data:array}} $result */ 42 | $result = $resp->getResult(); 43 | $shard = null; 44 | foreach ($result[0]['data'] as $row) { 45 | if ($row['Type'] === 'local') { 46 | $shard = $row['Agent']; 47 | break; 48 | } 49 | } 50 | if (!isset($shard)) { 51 | return TaskResult::withError('Failed to find structure from local shards'); 52 | } 53 | 54 | $q = match ($this->payload->type) { 55 | 'show' => "SHOW CREATE TABLE {$shard}", 56 | 'desc', 'describe' => "DESC {$shard}", 57 | default => throw new RuntimeException("Unknown type: {$this->payload->type}"), 58 | }; 59 | $resp = $this->manticoreClient->sendRequest($q); 60 | return TaskResult::fromResponse($resp); 61 | }; 62 | $task = Task::create($taskFn, []); 63 | return $task->run(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Plugin/Sharding/TableOperation.php: -------------------------------------------------------------------------------- 1 | table} OPTION force=1"; 48 | $resp = $manticoreClient->sendRequest($query); 49 | 50 | // It's important to have ` and 2 spaces for Apache Superset 51 | $resp->mapData( 52 | static function (array $row): array { 53 | /** @var array{'Create Table':string} $row */ 54 | $lines = explode("\n", $row['Create Table']); 55 | $lastN = sizeof($lines) - 1; 56 | foreach ($lines as $n => &$line) { 57 | if ($n === 0 || $n === $lastN) { 58 | continue; 59 | } 60 | $parts = explode(' ', $line); 61 | $parts[0] = '`' . trim($parts[0], '`') . '`'; 62 | $line = ' ' . trim(implode(' ', $parts)); 63 | } 64 | $row['Create Table'] = implode("\n", $lines); 65 | return $row; 66 | } 67 | ); 68 | 69 | return TaskResult::fromResponse($resp); 70 | }; 71 | 72 | return Task::create( 73 | $taskFn, 74 | [$this->payload, $this->manticoreClient] 75 | )->run(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Plugin/Show/SchemasHandler.php: -------------------------------------------------------------------------------- 1 | }} */ 49 | $result = $manticoreClient->sendRequest($query)->getResult(); 50 | return TaskResult::withData($result[0]['data']) 51 | ->column('Database', Column::String); 52 | }; 53 | 54 | return Task::create( 55 | $taskFn, 56 | [$this->manticoreClient] 57 | )->run(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Plugin/Show/VersionHandler.php: -------------------------------------------------------------------------------- 1 | sendRequest($query)->getResult())) 44 | ->column('Component', Column::String) 45 | ->column('Version', Column::String); 46 | }; 47 | 48 | return Task::create( 49 | $taskFn, [$this->manticoreClient] 50 | )->run(); 51 | } 52 | 53 | /** 54 | * @param Struct $result 55 | * @return array, array> 56 | */ 57 | private static function parseVersions(Struct $result):array { 58 | $versions = []; 59 | if (is_array($result[0]) && isset($result[0]['data'][0]['Value'])) { 60 | $value = $result[0]['data'][0]['Value']; 61 | 62 | $splittedVersions = explode('(', $value); 63 | 64 | foreach ($splittedVersions as $n => $version) { 65 | $version = trim($version); 66 | 67 | if ($version[mb_strlen($version) - 1] === ')') { 68 | $version = substr($version, 0, -1); 69 | } 70 | 71 | $exploded = explode(' ', $version); 72 | $component = $n > 0 ? ucfirst($exploded[0]) : 'Daemon'; 73 | 74 | $versions[] = ['Component' => $component, 'Version' => $version]; 75 | } 76 | } 77 | 78 | return $versions; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Plugin/Test/Handler.php: -------------------------------------------------------------------------------- 1 | 0) { 47 | Coroutine::sleep($timeout); 48 | } 49 | 50 | return TaskResult::none(); 51 | }; 52 | 53 | $task = Task::create($taskFn, [$this->payload->timeout]); 54 | if ($this->payload->isDeferred) { 55 | $task->defer(); 56 | } 57 | return $task->run(); 58 | } 59 | 60 | /** 61 | * @return array 62 | */ 63 | public function getProps(): array { 64 | return []; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Plugin/Test/Payload.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | final class Payload extends BasePayload { 22 | public function __construct(public int $timeout = 0, public bool $isDeferred = false) { 23 | } 24 | 25 | /** 26 | * Get description for this plugin 27 | * @return string 28 | */ 29 | public static function getInfo(): string { 30 | return 'Test plugin, used exclusively for tests'; 31 | } 32 | 33 | /** 34 | * @param Request $request 35 | * @return static 36 | */ 37 | public static function fromRequest(Request $request): static { 38 | // Request for Test command emulating hung Buddy requests 39 | // Contains info on request timeout and the type of the command's Task(deferred or not) 40 | // E.g.: test 6/deferred ; test 10 ; test deferred 41 | $self = new static(); 42 | $matches = []; 43 | preg_match('/^\s*test\s+(\d+)?\/?(deferred)?\s*$/i', $request->payload, $matches); 44 | $self->timeout = isset($matches[1]) ? abs((int)$matches[1]) : 0; 45 | $self->isDeferred = isset($matches[2]); 46 | 47 | return $self; 48 | } 49 | 50 | /** 51 | * @param Request $request 52 | * @return bool 53 | */ 54 | public static function hasMatch(Request $request): bool { 55 | return stripos($request->payload, 'test') === 0; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Plugin/Truncate/Handler.php: -------------------------------------------------------------------------------- 1 | manticoreClient->hasTable($this->payload->table); 39 | if (!$tableExists) { 40 | throw GenericError::create( 41 | "Table {$this->payload->table} does not exist" 42 | ); 43 | } 44 | 45 | $shards = $this->manticoreClient->getTableShards($this->payload->table); 46 | $requests = []; 47 | foreach ($shards as $shard) { 48 | $requests[] = [ 49 | 'url' => $shard['url'], 50 | 'path' => 'sql?mode=raw', 51 | 'request' => "TRUNCATE TABLE {$shard['name']}", 52 | ]; 53 | } 54 | 55 | $this->manticoreClient->sendMultiRequest($requests); 56 | return TaskResult::none(); 57 | }; 58 | 59 | return Task::create($taskFn)->run(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Plugin/Truncate/Payload.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | final class Payload extends BasePayload 24 | { 25 | public string $table; 26 | 27 | /** 28 | * Get description for this plugin 29 | * @return string 30 | */ 31 | public static function getInfo(): string { 32 | return 'Handles TRUNCATE statements on distributed tables'; 33 | } 34 | 35 | /** 36 | * @param Request $request 37 | * @return static 38 | */ 39 | public static function fromRequest(Request $request): static { 40 | $self = new static(); 41 | // Match truncate table pattern in the payload 42 | if (preg_match('/truncate\s+table\s+(?:`?([^`\s]+)`?)/i', $request->payload, $matches)) { 43 | // Extract table name from matches 44 | $self->table = $matches[1]; 45 | return $self; 46 | } 47 | 48 | throw QueryParseError::create('Failed to handle your TRUNCATE query', true); 49 | } 50 | 51 | /** 52 | * @param Request $request 53 | * @return bool 54 | */ 55 | public static function hasMatch(Request $request): bool { 56 | return $request->command === 'truncate' && 57 | stripos($request->error, 'requires an existing RT table') !== false; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Plugin/Update/Handler.php: -------------------------------------------------------------------------------- 1 | table} SET {$payload->setExpr} WHERE {$payload->whereExpr}"; 41 | $result = $client->sendRequest($stmt, path: null, disableAgentHeader: true); 42 | if ($result->hasError()) { 43 | throw GenericError::create( 44 | "Can't update table {$payload->table}" . 45 | 'Reason: ' . $result->getError() 46 | ); 47 | } 48 | 49 | return TaskResult::none(); 50 | }; 51 | 52 | return Task::create( 53 | $taskFn, [$this->payload, $this->manticoreClient] 54 | )->run(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/func.php: -------------------------------------------------------------------------------- 1 | execute('add', [$name, $value]); 32 | } 33 | 34 | /** 35 | * Little helper to check if we have telemetry enabled 36 | * 37 | * @return bool 38 | */ 39 | function is_telemetry_enabled(): bool { 40 | return ConfigManager::get('TELEMETRY', '1') === '1'; 41 | } 42 | 43 | /** 44 | * Little helper to convert config into int 45 | * @param string $val 46 | * @return int 47 | */ 48 | function return_bytes(string $val): int { 49 | $val = trim($val); 50 | $last = strtolower($val[strlen($val) - 1]); 51 | return (int)$val * match ($last) { 52 | 'g' => 1024 * 1024 * 1024, 53 | 'm' => 1024 * 1024, 54 | 'k' => 1024, 55 | default => 1, 56 | }; 57 | } 58 | 59 | 60 | /** 61 | * @param int $errno 62 | * @param string $errstr 63 | * @param string $errfile 64 | * @param int $errline 65 | * @return void 66 | */ 67 | function buddy_error_handler(int $errno, string $errstr, string $errfile, int $errline): void { 68 | throw new ErrorException($errstr, 0, $errno, $errfile, $errline); 69 | } 70 | 71 | /** 72 | * Crossplatform absolute path to project root of the Buddy sources 73 | * when running as Phar and not as Phar 74 | * @return string|bool 75 | */ 76 | function buddy_project_root(): string|bool { 77 | $projectRoot = class_exists('Phar') ? Phar::running(false) : false; 78 | if ($projectRoot) { 79 | $projectRoot = "phar://$projectRoot"; 80 | } else { 81 | $projectRoot = realpath( 82 | __DIR__ . DIRECTORY_SEPARATOR 83 | . '..' 84 | ); 85 | } 86 | 87 | return $projectRoot; 88 | } 89 | -------------------------------------------------------------------------------- /test/Buddy/functional/AutoSchemaSupportTest.php: -------------------------------------------------------------------------------- 1 | testTable}"); 26 | } 27 | 28 | protected function tearDown(): void { 29 | static::$manticoreConf = ''; 30 | } 31 | 32 | /** 33 | * Helper to setup auto schema 34 | * @param int $value can be 0 or 1 35 | * @return void 36 | */ 37 | protected function setUpAutoSchema(int $value): void { 38 | // Adding the auto schema option to manticore config 39 | $conf = str_replace( 40 | 'searchd {' . PHP_EOL, 41 | 'searchd {' . PHP_EOL . " auto_schema = $value" . PHP_EOL, 42 | static::$manticoreConf 43 | ); 44 | self::updateManticoreConf((string)$conf); 45 | echo $conf . PHP_EOL; 46 | 47 | // Restart manticore 48 | static::tearDownAfterClass(); 49 | sleep(5); // <- give 5 secs to protect from any kind of lags 50 | static::setUpBeforeClass(); 51 | } 52 | 53 | public function testAutoSchemaOptionDisabled(): void { 54 | echo "\nTesting the fail on the execution of HTTP insert query with searchd auto_schema=0\n"; 55 | $this->setUpAutoSchema(0); 56 | $query = "INSERT into {$this->testTable}(col1) VALUES(1) "; 57 | $out = static::runHttpQuery($query); 58 | $result = ['error' => "table 'test' absent"]; 59 | $this->assertEquals($result, $out); 60 | } 61 | 62 | public function testAutoSchemaOptionEnabled(): void { 63 | echo "\nTesting the fail on the execution of HTTP insert query with searchd auto_schema=1\n"; 64 | $this->setUpAutoSchema(1); 65 | $query = "INSERT into {$this->testTable}(col1) VALUES(1) "; 66 | $out = static::runHttpQuery($query); 67 | $result = [['total' => 1, 'error' => '','warning' => '']]; 68 | $this->assertEquals($result, $out); 69 | } 70 | 71 | public function testAutoSchemaOptionOmitted(): void { 72 | echo "\nTesting the fail on the execution of HTTP insert query without searchd auto_schema set\n"; 73 | $query = "INSERT into {$this->testTable}(col1,col2) VALUES(1,2) "; 74 | $out = static::runHttpQuery($query); 75 | $result = [['total' => 1,'error' => '','warning' => '']]; 76 | $this->assertEquals($result, $out); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /test/Buddy/functional/BackupTest.php: -------------------------------------------------------------------------------- 1 | assertQueryResultContainsError('backup', 'You have an error in your query. Please, double check it.'); 21 | $this->assertQueryResultContainsError('backup to /tmp', 'You have no tables to backup.'); 22 | $this->assertQueryResultContainsError( 23 | 'backup tables a to /unexisting/dir', 24 | 'Backup directory is not writable' 25 | ); 26 | $this->assertQueryResultContainsError('backup table c to /tmp', "Can't find some of the tables: c"); 27 | } 28 | 29 | public function testBackupWorksWell(): void { 30 | // Prepare some tables first 31 | static::runSqlQuery('create table a'); 32 | static::runSqlQuery('create table b'); 33 | 34 | exec('rm -fr /tmp/backup1 /tmp/backup2 /tmp/backup3'); 35 | exec('mkdir -p /tmp/backup1 /tmp/backup2 /tmp/backup3'); 36 | 37 | $this->assertQueryResult('backup to /tmp/backup1', 'Path: /tmp/backup1/backup-'); 38 | $this->assertQueryResult('backup tables a, b to /tmp/backup2', 'Path: /tmp/backup2/backup-'); 39 | $this->assertQueryResult('backup table a to /tmp/backup3', 'Path: /tmp/backup3/backup-'); 40 | 41 | static::runSqlQuery('drop table if exists a'); 42 | static::runSqlQuery('drop table if exists b'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/Buddy/functional/BuildTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(true, file_exists('build/share/modules/manticore-buddy/src/main.php')); 22 | $this->assertEquals(true, file_exists('build/manticore-buddy')); 23 | } 24 | 25 | public function testBuildHasRightComposerPackages(): void { 26 | /** @var array{require:array,require-dev:array} $composer */ 27 | $composer = simdjson_decode((string)file_get_contents('/workdir/composer.json'), true); 28 | $include = array_keys($composer['require']); 29 | $exclude = array_keys($composer['require-dev']); 30 | $vendorPath = 'build/share/modules/manticore-buddy/vendor'; 31 | /** @var array{dev:bool,dev-package-names:array} $installed */ 32 | $installed = simdjson_decode( 33 | (string)file_get_contents("$vendorPath/composer/installed.json"), 34 | true 35 | ); 36 | $this->assertEquals(false, $installed['dev']); 37 | $this->assertEquals([], $installed['dev-package-names']); 38 | 39 | 40 | $vendorPathIterator = new RecursiveDirectoryIterator($vendorPath); 41 | $vendorPathLen = strlen($vendorPath); 42 | $packages = []; 43 | /** @var SplFileInfo $file */ 44 | foreach (new RecursiveIteratorIterator($vendorPathIterator) as $file) { 45 | $ns = strtok(substr((string)$file, $vendorPathLen), '/'); 46 | $name = strtok('/'); 47 | $packages["$ns/$name"] = true; 48 | } 49 | 50 | $packages = array_keys($packages); 51 | $this->assertEquals([], array_diff($include, $packages)); 52 | $this->assertEquals($exclude, array_diff($exclude, $packages)); 53 | } 54 | 55 | protected static function buildBinary(): void { 56 | system('phar_builder/bin/build --name="Manticore Buddy" --package="manticore-buddy"'); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/Buddy/functional/DebugModeTest.php: -------------------------------------------------------------------------------- 1 | searchdLogFilepath = $matches[1]; 35 | $this->searchdLog = (string)file_get_contents($this->searchdLogFilepath); 36 | self::setUpBeforeClass(); 37 | } 38 | 39 | public function tearDown(): void { 40 | self::tearDownAfterClass(); 41 | } 42 | 43 | public function testDebugModeOff(): void { 44 | echo "\nTesting the Buddy log output without debug mode enabled\n"; 45 | ob_flush(); 46 | // Waiting for the possible debug message to come 47 | sleep(70); 48 | // Checking the log part corresponding to the latest searchd start 49 | $logUpdate = str_replace($this->searchdLog, '', (string)file_get_contents($this->searchdLogFilepath)); 50 | $this->assertStringNotContainsString('[BUDDY] memory usage:', $logUpdate); 51 | static::setSearchdArgs(['--log-level=debugv']); 52 | } 53 | 54 | /** 55 | * @depends testDebugModeOff 56 | */ 57 | public function testDebugModeOn(): void { 58 | echo "\nTesting the Buddy log output with debug mode enabled\n"; 59 | ob_flush(); 60 | // Waiting for the possible debug message to come 61 | sleep(70); 62 | // Checking the log part corresponding to the latest searchd start 63 | $logUpdate = str_replace($this->searchdLog, '', (string)file_get_contents($this->searchdLogFilepath)); 64 | $this->assertStringContainsString('[BUDDY] memory usage:', $logUpdate); 65 | static::setSearchdArgs([]); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /test/Buddy/functional/ListenArgTest.php: -------------------------------------------------------------------------------- 1 | defaultPort = $this->getListenDefaultPort(); 26 | } 27 | 28 | protected function tearDown(): void { 29 | // Restoring listen default port 30 | $this->setListenDefaultPort($this->defaultPort); 31 | } 32 | 33 | public function testListenArgumentChange(): void { 34 | echo "\nTesting if the `listen` argument is passed from daemon to Buddy correctly\n"; 35 | $this->setListenDefaultPort(8888); 36 | $httpPort = self::getListenHttpPort(); 37 | exec("curl localhost:$httpPort/sql?mode=raw -d 'query=drop table if exists test' 2>&1"); 38 | $query = 'INSERT into test(col1) VALUES(1) '; 39 | exec("curl localhost:$httpPort/sql?mode=raw -d 'query=$query' 2>&1", $out); 40 | $result = '[{"total":1,"error":"","warning":""}]'; 41 | $this->assertEquals($result, $out[3]); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /test/Buddy/functional/ProcessErrorTest.php: -------------------------------------------------------------------------------- 1 | assertQueryResultContainsError( 21 | 'tratatata', 22 | "P02: syntax error, unexpected identifier near 'tratatata'" 23 | ); 24 | $this->assertQueryResultContainsError( 25 | 'hello how are you?', 26 | "P02: syntax error, unexpected identifier near 'hello how are you?'" 27 | ); 28 | $this->assertQueryResultContainsError( 29 | 'showf tables', 30 | "P02: syntax error, unexpected identifier near 'showf tables'" 31 | ); 32 | } 33 | 34 | public function testCorrectErrorOnBackupNoTables(): void { 35 | $this->assertQueryResultContainsError( 36 | 'backup to /tmp', 37 | 'You have no tables to backup.' 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/Buddy/functional/ShowOpenTablesTest.php: -------------------------------------------------------------------------------- 1 | assertQueryResult( 40 | 'SHOW OPEN TABLES', [ 41 | 'Table: a', 42 | 'Table: b', 43 | 'Table: test123', 44 | 'Table: hello', 45 | ] 46 | ); 47 | } 48 | 49 | public function testShowOpenTablesFiltersLikeInAProperWay(): void { 50 | $this->assertQueryResult( 51 | "SHOW OPEN TABLES LIKE 'a'", [ 52 | 'Table: a', 53 | ], [ 54 | 'Table: b', 55 | 'Table: test123', 56 | 'Table: hello', 57 | ] 58 | ); 59 | 60 | $this->assertQueryResult( 61 | "SHOW OPEN TABLES LIKE 'doesnotexist'", [ 62 | 63 | ], [ 64 | 'Table: a', 65 | 'Table: b', 66 | 'Table: test123', 67 | 'Table: hello', 68 | ] 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /test/Buddy/functional/ShowQueriesTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($expectedFields, $realFields); 32 | $query = ' show queries '; 33 | $out = static::runSqlQuery($query); 34 | 35 | $realFields = array_values(array_filter(array_map('trim', explode('|', $out[1])))); 36 | $this->assertEquals($expectedFields, $realFields); 37 | } 38 | 39 | public function testSQLShowQueriesFail(): void { 40 | echo "\nTesting the fail on the execution of SQL SHOW QUERIES statement\n"; 41 | $query = 'SHOW QUERIES 123'; 42 | $out = static::runSqlQuery($query); 43 | $result = [ 44 | 'ERROR 1064 (42000) at line 1: P01: syntax error, unexpected identifier, ' 45 | . "expecting VARIABLES near 'QUERIES 123'", 46 | ]; 47 | $this->assertEquals($result, $out); 48 | } 49 | 50 | public function testHTTPShowQueriesOk(): void { 51 | echo "\nTesting the execution of HTTP SHOW QUERIES statement\n"; 52 | $query = 'SHOW QUERIES'; 53 | $out = static::runHttpQuery($query); 54 | $resultColumns = [ 55 | ['id' => ['type' => 'long long']], 56 | ['query' => ['type' => 'string']], 57 | ['time' => ['type' => 'string']], 58 | ['protocol' => ['type' => 'string']], 59 | ['host' => ['type' => 'string']], 60 | ]; 61 | if (!(isset($out[0]['columns'], $out[0]['total']))) { 62 | $this->fail('Unexpected response from searchd'); 63 | } 64 | $this->assertEquals('', $out[0]['error']); 65 | $this->assertGreaterThan(0, $out[0]['total']); 66 | $this->assertEquals($resultColumns, $out[0]['columns']); 67 | } 68 | 69 | public function testHTTPShowQueriesFail(): void { 70 | echo "\nTesting the fail on the execution of HTTP SHOW QUERIES statement\n"; 71 | $query = 'SHOW QUERIES 123'; 72 | $out = static::runHttpQuery($query); 73 | $result = [ 74 | 'error' => "P01: syntax error, unexpected identifier, expecting VARIABLES near 'QUERIES 123'", 75 | ]; 76 | $this->assertEquals($result, $out); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /test/Buddy/functional/ShowVariablesTest.php: -------------------------------------------------------------------------------- 1 | assertQueryResult($query, ['Variable_name', 'Value', ...static::FIELDS]); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/Buddy/functional/config/manticore-bench.conf: -------------------------------------------------------------------------------- 1 | common { 2 | plugin_dir = /usr/local/lib/manticore 3 | lemmatizer_base = /usr/share/manticore/morph/ 4 | } 5 | searchd { 6 | listen = 0.0.0.0:8312 7 | listen = 0.0.0.0:8306:mysql 8 | listen = 0.0.0.0:8308:http 9 | log = /var/log/manticore-test/searchd.log 10 | query_log = /var/log/manticore-test/query.log 11 | pid_file = /var/run/manticore-test/searchd.pid 12 | data_dir = /var/lib/manticore-test 13 | query_log_format = sphinxql 14 | buddy_path = manticore-executor /workdir/src/main.php 15 | threads = 2 16 | } 17 | -------------------------------------------------------------------------------- /test/Buddy/functional/config/manticore.conf: -------------------------------------------------------------------------------- 1 | common { 2 | plugin_dir = /usr/local/lib/manticore 3 | lemmatizer_base = /usr/share/manticore/morph/ 4 | } 5 | searchd { 6 | listen = 0.0.0.0:8312 7 | listen = 0.0.0.0:8306:mysql 8 | listen = 0.0.0.0:8308:http 9 | log = /var/log/manticore-test/searchd.log 10 | query_log = /var/log/manticore-test/query.log 11 | pid_file = /var/run/manticore-test/searchd.pid 12 | data_dir = /var/lib/manticore-test 13 | query_log_format = sphinxql 14 | buddy_path = manticore-executor /workdir/src/main.php --telemetry-period=10 15 | threads = 4 16 | } 17 | -------------------------------------------------------------------------------- /test/Kafka/import.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | bootstrap_servers='localhost:9092' 4 | kafka_topic='my-data' 5 | json_file_path='/tmp/dump.json' 6 | 7 | 8 | /opt/bitnami/kafka/bin/kafka-console-producer.sh \ 9 | --broker-list "$bootstrap_servers" \ 10 | --property parse.key=true \ 11 | --property "key.separator=:" \ 12 | --topic "$kafka_topic" < "$json_file_path" 13 | -------------------------------------------------------------------------------- /test/Plugin/CliTable/CliTableHandlerTest.php: -------------------------------------------------------------------------------- 1 | '', 45 | 'payload' => 'SHOW QUERIES', 46 | 'version' => Buddy::PROTOCOL_VERSION, 47 | 'format' => RequestFormat::SQL, 48 | 'endpointBundle' => ManticoreEndpoint::Cli, 49 | 'path' => 'cli', 50 | ] 51 | ); 52 | 53 | self::setBuddyVersion(); 54 | $serverUrl = self::setUpMockManticoreServer(false); 55 | $manticoreClient = new HTTPClient($serverUrl); 56 | $manticoreClient->setForceSync(true); 57 | Payload::$type = 'queries'; 58 | $payload = Payload::fromRequest($request); 59 | $handler = new Handler($payload); 60 | $refCls = new ReflectionClass($handler); 61 | $refCls->getProperty('manticoreClient')->setValue($handler, $manticoreClient); 62 | go( 63 | function () use ($handler, $respBody) { 64 | $task = $handler->run(); 65 | $task->wait(true); 66 | 67 | $this->assertEquals(true, $task->isSucceed()); 68 | $result = $task->getResult()->getTableFormatted(0); 69 | $this->assertIsString($result); 70 | $this->assertStringContainsString($respBody, $result); 71 | self::finishMockManticoreServer(); 72 | } 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /test/Plugin/Insert/Exception/ParserLoadErrorTest.php: -------------------------------------------------------------------------------- 1 | expectException(ParserLoadError::class); 20 | $this->expectExceptionMessage('Test error message'); 21 | throw new ParserLoadError('Test error message'); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /test/Plugin/Insert/InsertQuery/InsertQueryPayloadTest.php: -------------------------------------------------------------------------------- 1 | Buddy::PROTOCOL_VERSION, 25 | 'error' => '', 26 | 'payload' => 'INSERT INTO test(int_col, string_col, float_col, @timestamp)' 27 | . ' VALUES(1, \'string\', 2.22, \'2000-01-01T12:00:00Z\')', 28 | 'format' => RequestFormat::SQL, 29 | 'endpointBundle' => ManticoreEndpoint::Sql, 30 | 'path' => 'sql?mode=raw', 31 | ] 32 | ); 33 | $payload = Payload::fromRequest($request); 34 | $this->assertInstanceOf(Payload::class, $payload); 35 | 36 | echo "\nTesting the prepared quries after creating request are correct\n"; 37 | 38 | $this->assertIsArray($payload->queries); 39 | $this->assertEquals(2, sizeof($payload->queries)); 40 | $this->assertEquals( 41 | [ 42 | 'CREATE TABLE IF NOT EXISTS `test` (`int_col` int,`string_col` text,`float_col` float,' 43 | . '`@timestamp` timestamp)', 44 | 'INSERT INTO test(int_col, string_col, float_col, @timestamp) VALUES(1, \'string\', 2.22,' 45 | . ' \'2000-01-01T12:00:00Z\')', 46 | ], $payload->queries 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/Plugin/Insert/QueryParser/ParserLoaderTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(SQLInsertParser::class, $parser); 26 | $this->assertInstanceOf(InsertQueryParserInterface::class, $parser); 27 | $parser = Loader::getInsertQueryParser('test', Endpoint::Cli); 28 | $this->assertInstanceOf(SQLInsertParser::class, $parser); 29 | echo "\nGetting JSONInsertParser instance\n"; 30 | $parser = Loader::getInsertQueryParser('insert', Endpoint::Insert); 31 | $this->assertInstanceOf(JSONInsertParser::class, $parser); 32 | try { 33 | $this->assertInstanceOf(ElasticJSONInsertParser::class, $parser); 34 | $this->fail(); 35 | } catch (Exception) { 36 | } 37 | $parser = Loader::getInsertQueryParser('test/_doc/', Endpoint::Bulk); 38 | $this->assertInstanceOf(ElasticJSONInsertParser::class, $parser); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /test/Plugin/Sharding/NodeTest.php: -------------------------------------------------------------------------------- 1 | 'localhost:9312', 17 | 'dfsdfsdf' => null, 18 | 'hello:world' => null, 19 | 'domain.local:9312' => 'domain.local:9312', 20 | '127.0.0.1:9312' => '127.0.0.1:9312', 21 | '127.0.0.1:9306:mysql' => null, 22 | '127.0.0.1:9308:http' => '127.0.0.1:9308', 23 | '9333' => '127.0.0.1:9333', 24 | '9234:http' => '127.0.0.1:9234', 25 | ]; 26 | 27 | /** 28 | * Validate we can parse valid lines from config to get Node ID 29 | * @return void 30 | */ 31 | public function testNodeIdParsing(): void { 32 | echo "\nTesting the parsing of node id from line\n"; 33 | $map = static::TEST_MAP; 34 | // Edge case when we should detect hostname 35 | $hostname = gethostname(); 36 | $host = gethostbyname($hostname ?: ''); 37 | $map['0.0.0.0:9552'] = "$host:9552"; 38 | $map['0.0.0.0:9552:http'] = "$host:9552"; 39 | foreach ($map as $line => $expected) { 40 | // PHP has a bug and converts string into ints when can 41 | $nodeId = Node::parseNodeId((string)$line); 42 | $this->assertEquals($expected, $nodeId); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/bootstrap.php: -------------------------------------------------------------------------------- 1 | get('manticoreClient'); 42 | $manticoreClient->setServerUrl('127.0.0.1:8312'); 43 | MetricThread::setContainer($container); 44 | // phpcs:enable 45 | -------------------------------------------------------------------------------- /test/src/Lib/BuddyRequestError.php: -------------------------------------------------------------------------------- 1 | $args 22 | * @return mixed 23 | */ 24 | public static function invokeMethod(mixed $classInstance, string $methodName, array $args = []): mixed { 25 | $class = new \ReflectionClass($classInstance); 26 | $method = $class->getMethod($methodName); 27 | $method->setAccessible(true); 28 | $ref = gettype($classInstance) === 'string' ? null : $classInstance; 29 | return $method->invokeArgs($ref, $args); 30 | } 31 | 32 | /** 33 | * @param class-string|object $classInstance 34 | * @param string $methodName 35 | * @param array $args 36 | * @return array{0:string,1:string} 37 | */ 38 | public static function getExceptionInfo(mixed $classInstance, string $methodName, array $args = []): array { 39 | $exCls = $exMsg = ''; 40 | try { 41 | self::invokeMethod($classInstance, $methodName, $args); 42 | } catch (GenericError $e) { 43 | $exCls = $e::class; 44 | $exMsg = $e->getMessage(); 45 | } 46 | 47 | return [$exCls, $exMsg]; 48 | } 49 | } 50 | --------------------------------------------------------------------------------