Repository: shlima/click_house Branch: master Commit: 663b72d42fbc Files: 124 Total size: 151.3 KB Directory structure: gitextract_7a4m72cg/ ├── .github/ │ └── workflows/ │ └── main.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── CHANGELOG.md ├── Gemfile ├── Gemfile_faraday1 ├── LICENCE.txt ├── Makefile ├── README.md ├── Rakefile ├── bin/ │ ├── console │ ├── release.sh │ └── setup ├── click_house.gemspec ├── docker-compose.yml ├── lib/ │ ├── click_house/ │ │ ├── ast/ │ │ │ ├── parser.rb │ │ │ ├── statement.rb │ │ │ └── ticker.rb │ │ ├── ast.rb │ │ ├── benchmark/ │ │ │ ├── casting.rb │ │ │ └── map_join.rb │ │ ├── config.rb │ │ ├── connection.rb │ │ ├── definition/ │ │ │ ├── column.rb │ │ │ └── column_set.rb │ │ ├── definition.rb │ │ ├── errors.rb │ │ ├── extend/ │ │ │ ├── configurable.rb │ │ │ ├── connectible.rb │ │ │ ├── connection_altering.rb │ │ │ ├── connection_database.rb │ │ │ ├── connection_explaining.rb │ │ │ ├── connection_healthy.rb │ │ │ ├── connection_inserting.rb │ │ │ ├── connection_selective.rb │ │ │ ├── connection_table.rb │ │ │ └── type_definition.rb │ │ ├── extend.rb │ │ ├── middleware/ │ │ │ ├── logging.rb │ │ │ ├── parse_csv.rb │ │ │ ├── parse_json.rb │ │ │ ├── parse_json_oj.rb │ │ │ ├── raise_error.rb │ │ │ ├── response_base.rb │ │ │ └── summary_middleware.rb │ │ ├── middleware.rb │ │ ├── response/ │ │ │ ├── factory.rb │ │ │ ├── result_set.rb │ │ │ └── summary.rb │ │ ├── response.rb │ │ ├── serializer/ │ │ │ ├── base.rb │ │ │ ├── json_oj_serializer.rb │ │ │ └── json_serializer.rb │ │ ├── serializer.rb │ │ ├── type/ │ │ │ ├── array_type.rb │ │ │ ├── base_type.rb │ │ │ ├── boolean_type.rb │ │ │ ├── date_time64_type.rb │ │ │ ├── date_time_type.rb │ │ │ ├── date_type.rb │ │ │ ├── decimal_type.rb │ │ │ ├── fixed_string_type.rb │ │ │ ├── float_type.rb │ │ │ ├── integer_type.rb │ │ │ ├── ip_type.rb │ │ │ ├── low_cardinality_type.rb │ │ │ ├── map_type.rb │ │ │ ├── nullable_type.rb │ │ │ ├── string_type.rb │ │ │ ├── tuple_type.rb │ │ │ └── undefined_type.rb │ │ ├── type.rb │ │ ├── util/ │ │ │ ├── pretty.rb │ │ │ └── statement.rb │ │ ├── util.rb │ │ └── version.rb │ └── click_house.rb ├── log/ │ └── .keep ├── spec/ │ ├── click_house/ │ │ ├── ast/ │ │ │ └── parser_spec.rb │ │ ├── config_spec.rb │ │ ├── connection_spec.rb │ │ ├── definition/ │ │ │ └── column_set_spec.rb │ │ ├── extend/ │ │ │ ├── connection_altering_spec.rb │ │ │ ├── connection_database_spec.rb │ │ │ ├── connection_explaining_spec.rb │ │ │ ├── connection_healthy_spec.rb │ │ │ ├── connection_inserting_spec.rb │ │ │ ├── connection_selective_spec.rb │ │ │ └── connection_table_spec.rb │ │ ├── integration/ │ │ │ ├── array_spec.rb │ │ │ ├── boolean_type_spec.rb │ │ │ ├── date_spec.rb │ │ │ ├── date_time64_spec.rb │ │ │ ├── date_time_spec.rb │ │ │ ├── decimal_spec.rb │ │ │ ├── enum_spec.rb │ │ │ ├── float_spec.rb │ │ │ ├── formats.rb │ │ │ ├── function_spec.rb │ │ │ ├── integer_spec.rb │ │ │ ├── ip_spec.rb │ │ │ ├── loggin_spec.rb │ │ │ ├── low_cardinality_spec.rb │ │ │ ├── map_spec.rb │ │ │ ├── nested_spec.rb │ │ │ ├── string_spec.rb │ │ │ ├── symbolize_keys_spec.rb │ │ │ ├── table_schema_spec.rb │ │ │ └── tuple_spec.rb │ │ ├── response/ │ │ │ └── factory_spec.rb │ │ ├── type/ │ │ │ ├── date_time64_spec.rb │ │ │ ├── date_time_type_spec.rb │ │ │ ├── decimal_type_spec.rb │ │ │ ├── fixed_string_type_spec.rb │ │ │ ├── float_type_spec.rb │ │ │ └── ip_type_spec.rb │ │ └── util/ │ │ └── pretty_spec.rb │ ├── oj_helper.rb │ ├── spec_helper.rb │ └── support/ │ ├── database_cleaner.rb │ ├── reset_connection.rb │ └── ruby_version.rb └── tmp/ └── .keep ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/main.yml ================================================ name: CI on: [push, pull_request] jobs: rspec: runs-on: ubuntu-latest services: clickhouse: image: clickhouse/clickhouse-server:22.9 ports: - 8123:8123 strategy: matrix: ruby-version: [3.1, '3.0', 2.7] steps: - uses: actions/checkout@v2 - name: Set up Ruby ${{ matrix.ruby-version }} uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} - name: Setup v1 run: make faraday1 bundle - name: Setup v2 run: make faraday2 bundle - name: Run tests with faraday v.1 JSON run: make faraday1 rspec - name: Run tests with faraday v.2 JSON run: make faraday2 rspec - name: Run tests with faraday v.1 OJ run: make faraday1 oj rspec - name: Run tests with faraday v.2 OJ run: make faraday2 oj rspec rubocop: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Ruby 2.7 uses: ruby/setup-ruby@v1 with: ruby-version: 2.7 bundler-cache: true # 'bundle install' and cache - name: Run Rubocop run: bundle exec rubocop ================================================ FILE: .gitignore ================================================ /.bundle/ /.yardoc /.idea/ /_yardoc/ /coverage/ /pkg/ /spec/reports/ /log/* /tmp/* /*.gem !/log/.keep !/tmp/.keep # rspec failure tracking .rspec_status ================================================ FILE: .rspec ================================================ --format documentation --color --require spec_helper ================================================ FILE: .rubocop.yml ================================================ require: - rubocop-performance AllCops: AutoCorrect: false SuggestExtensions: false Exclude: - 'click_house.gemspec' - 'bin/*' - 'lib/click_house/benchmark/*' - 'spec/**/*' - 'vendor/**/*' TargetRubyVersion: 2.7 Bundler/OrderedGems: Enabled: false # ============================== Gemspec ====================== Gemspec/DeprecatedAttributeAssignment: Enabled: true Gemspec/RequireMFA: # new in 1.23 Enabled: true # =============================== Performance ======================= Performance/AncestorsInclude: Enabled: true Performance/BigDecimalWithNumericArgument: Enabled: true Performance/RedundantSortBlock: Enabled: true Performance/RedundantStringChars: Enabled: true Performance/ReverseFirst: Enabled: true Performance/SortReverse: Enabled: true Performance/Squeeze: Enabled: true Performance/StringInclude: Enabled: true Performance/Sum: Enabled: true Performance/ArraySemiInfiniteRangeSlice: Enabled: true Performance/BlockGivenWithExplicitBlock: Enabled: true Performance/CollectionLiteralInLoop: Enabled: true Performance/ConstantRegexp: Enabled: true Performance/MethodObjectAsBlock: Enabled: false Performance/RedundantEqualityComparisonBlock: Enabled: true Performance/RedundantSplitRegexpArgument: Enabled: true Performance/MapCompact: Enabled: true Performance/ConcurrentMonotonicTime: # new in 1.12 Enabled: true Performance/StringIdentifierArgument: # new in 1.13 Enabled: true # ============================== Metrics ============================ Metrics/ClassLength: Max: 180 Metrics/BlockLength: Enabled: true Metrics/MethodLength: Max: 25 Metrics/AbcSize: Max: 40 # ============================== Naming ============================= Naming/PredicateName: ForbiddenPrefixes: - is_ Naming/FileName: Enabled: true Exclude: - 'Gemfile' Naming/MethodParameterName: Enabled: false Naming/AccessorMethodName: Enabled: false Naming/InclusiveLanguage: Enabled: true Naming/BlockForwarding: # new in 1.24 Enabled: true # ============================== Layout ============================= Layout/LineLength: Max: 140 Layout/HashAlignment: EnforcedHashRocketStyle: key EnforcedColonStyle: key Layout/ParameterAlignment: EnforcedStyle: with_fixed_indentation Layout/CaseIndentation: EnforcedStyle: case IndentOneStep: false Layout/MultilineMethodCallIndentation: Enabled: true EnforcedStyle: indented Layout/SpaceBeforeBlockBraces: EnforcedStyle: space EnforcedStyleForEmptyBraces: space Layout/EmptyLines: Enabled: true Layout/EmptyLineAfterMagicComment: Enabled: false Layout/EmptyLinesAroundBlockBody: Enabled: true Layout/EndAlignment: EnforcedStyleAlignWith: variable Layout/FirstHashElementIndentation: EnforcedStyle: consistent Layout/HeredocIndentation: Enabled: false Layout/RescueEnsureAlignment: Enabled: false Layout/EmptyLinesAroundAttributeAccessor: Enabled: true Layout/SpaceAroundMethodCallOperator: Enabled: true Layout/SpaceBeforeBrackets: Enabled: true Layout/LineEndStringConcatenationIndentation: Enabled: true Layout/LineContinuationLeadingSpace: # new in 1.31 Enabled: true Layout/LineContinuationSpacing: # new in 1.31 Enabled: true # ============================== Style ============================== Style/RescueModifier: Enabled: true Style/PercentLiteralDelimiters: PreferredDelimiters: default: '[]' '%i': '[]' '%w': '[]' Exclude: - 'config/routes.rb' Style/StringLiterals: Enabled: true Style/AsciiComments: Enabled: false Style/Copyright: Enabled: false Style/SafeNavigation: Enabled: false Style/Lambda: Enabled: false Style/Alias: Enabled: true EnforcedStyle: prefer_alias_method Style/ClassAndModuleChildren: Enabled: true EnforcedStyle: nested Style/TrailingCommaInArrayLiteral: Enabled: true EnforcedStyleForMultiline: no_comma Style/RescueStandardError: Enabled: true EnforcedStyle: explicit Style/InverseMethods: AutoCorrect: false Enabled: true Style/IfUnlessModifier: Enabled: false Style/SpecialGlobalVars: Enabled: false Style/BlockComments: Enabled: false Style/GuardClause: Enabled: false Style/TrailingCommaInHashLiteral: Enabled: false Style/ExponentialNotation: Enabled: true Style/HashEachMethods: Enabled: true Style/HashTransformKeys: Enabled: true Style/HashTransformValues: Enabled: true Style/RedundantFetchBlock: Enabled: true Style/RedundantRegexpCharacterClass: Enabled: true Style/RedundantRegexpEscape: Enabled: true Style/SlicingWithRange: Enabled: true Style/AccessorGrouping: Enabled: false Style/ArrayCoercion: Enabled: true Style/BisectedAttrAccessor: Enabled: true Style/CaseLikeIf: Enabled: true Style/HashAsLastArrayItem: Enabled: true Style/HashLikeCase: Enabled: true Style/RedundantAssignment: Enabled: true Style/RedundantFileExtensionInRequire: Enabled: true Style/ExplicitBlockArgument: Enabled: true Style/GlobalStdStream: Enabled: true Style/OptionalBooleanParameter: Enabled: true Style/SingleArgumentDig: Enabled: true Style/StringConcatenation: Enabled: true Style/ClassEqualityComparison: Enabled: true Style/CombinableLoops: Enabled: true Style/KeywordParametersOrder: Enabled: false Style/RedundantSelfAssignment: Enabled: true Style/SoleNestedConditional: Enabled: true Style/ArgumentsForwarding: Enabled: true Style/CollectionCompact: Enabled: true Style/DocumentDynamicEvalDefinition: Enabled: false Style/NegatedIfElseCondition: Enabled: true Style/NilLambda: Enabled: true Style/SwapValues: Enabled: true Style/RedundantArgument: Enabled: true Style/HashExcept: Enabled: true Style/EndlessMethod: Enabled: true Style/IfWithBooleanLiteralBranches: Enabled: true Style/HashConversion: Enabled: true Style/Documentation: Enabled: false Style/InPatternThen: Enabled: true Style/MultilineInPatternThen: Enabled: true Style/QuotedSymbols: Enabled: true Style/StringChars: Enabled: true Style/EmptyHeredoc: # new in 1.32 Enabled: true Style/EnvHome: # new in 1.29 Enabled: true Style/FetchEnvVar: # new in 1.28 Enabled: true Style/FileRead: # new in 1.24 Enabled: true Style/FileWrite: # new in 1.24 Enabled: true Style/MagicCommentFormat: # new in 1.35 Enabled: true Style/MapCompactWithConditionalBlock: # new in 1.30 Enabled: true Style/MapToHash: # new in 1.24 Enabled: true Style/NestedFileDirname: # new in 1.26 Enabled: true Style/NumberedParameters: # new in 1.22 Enabled: true Style/NumberedParametersLimit: # new in 1.22 Enabled: true Style/ObjectThen: # new in 1.28 Enabled: true Style/OpenStructUse: # new in 1.23 Enabled: true Style/OperatorMethodCall: # new in 1.37 Enabled: true Style/RedundantEach: # new in 1.38 Enabled: true Style/RedundantInitialize: # new in 1.27 Enabled: true Style/RedundantSelfAssignmentBranch: # new in 1.19 Enabled: true Style/RedundantStringEscape: # new in 1.37 Enabled: true Style/SelectByRegexp: # new in 1.22 Enabled: true # ============================== Security ============================== Security/CompoundHash: # new in 1.28 Enabled: true Security/IoMethods: # new in 1.22 Enabled: true # ============================== Lint ============================== Lint/DuplicateMethods: Enabled: false Lint/AmbiguousOperator: Enabled: false Lint/DeprecatedOpenSSLConstant: Enabled: true Lint/MixedRegexpCaptureTypes: Enabled: true Lint/RaiseException: Enabled: true Lint/StructNewOverride: Enabled: true Lint/DuplicateElsifCondition: Enabled: true Lint/BinaryOperatorWithIdenticalOperands: Enabled: true Lint/DuplicateRescueException: Enabled: true Lint/EmptyConditionalBody: Enabled: true Lint/FloatComparison: Enabled: true Lint/MissingSuper: Enabled: false Lint/OutOfRangeRegexpRef: Enabled: true Lint/SelfAssignment: Enabled: true Lint/TopLevelReturnWithArgument: Enabled: true Lint/UnreachableLoop: Enabled: true Layout/BeginEndAlignment: Enabled: true Lint/ConstantDefinitionInBlock: Enabled: true Lint/DuplicateRequire: Enabled: true Lint/EmptyFile: Enabled: true Lint/HashCompareByIdentity: Enabled: true Lint/IdentityComparison: Enabled: true Lint/RedundantSafeNavigation: Enabled: true Lint/TrailingCommaInAttributeDeclaration: Enabled: true Lint/UselessMethodDefinition: Enabled: true Lint/UselessTimes: Enabled: true Lint/DuplicateBranch: Enabled: true Lint/DuplicateRegexpCharacterClassElement: Enabled: true Lint/EmptyBlock: Enabled: true Lint/EmptyClass: Enabled: true Lint/NoReturnInBeginEndBlocks: Enabled: true Lint/ToEnumArguments: Enabled: true Lint/UnmodifiedReduceAccumulator: Enabled: true Lint/UnexpectedBlockArity: Enabled: true Lint/DeprecatedConstants: Enabled: true Lint/LambdaWithoutLiteralBlock: Enabled: true Lint/NumberedParameterAssignment: Enabled: true Lint/OrAssignmentToConstant: Enabled: true Lint/RedundantDirGlobSort: Enabled: true Lint/SymbolConversion: Enabled: true Lint/TripleQuotes: Enabled: true Lint/AmbiguousAssignment: Enabled: true Lint/EmptyInPattern: Enabled: true Lint/AmbiguousOperatorPrecedence: # new in 1.21 Enabled: true Lint/AmbiguousRange: # new in 1.19 Enabled: true Lint/ConstantOverwrittenInRescue: # new in 1.31 Enabled: true Lint/DuplicateMagicComment: # new in 1.37 Enabled: true Lint/IncompatibleIoSelectWithFiberScheduler: # new in 1.21 Enabled: true Lint/NonAtomicFileOperation: # new in 1.31 Enabled: true Lint/RefinementImportMethods: # new in 1.27 Enabled: true Lint/RequireRangeParentheses: # new in 1.32 Enabled: true Lint/RequireRelativeSelfPath: # new in 1.22 Enabled: true Lint/UselessRuby2Keywords: # new in 1.23 Enabled: true ================================================ FILE: CHANGELOG.md ================================================ # 2.1.1 * Fix logging with symbolized keys JSON * Unknown formats return raw `Response::ResultSelt` like regular JSON query * Added methods `statistics`, `summary`, `headers` and `types` to `Response::ResultSet` # 2.1.0 * `ClickHouse.connection.insert` now returns `ClickHouse::Response::Summary` object with methods `headers`, `summary`, `written_rows`, `written_bytes`, etc... * `ClickHouse.connection.insert(columns: ["id"], values: [1])` now uses `JSONCompactEachRow` by default (to increase JSON serialization speed) * Methods `insert_rows` and `insert_compact` added to `connection` * Added ability to pass object directly to insert like: `ClickHouse.connection.insert("table", {id: 1})` or `ClickHouse.connection.insert("table", [{id: 1})]` (for ruby < 3.0 use `ClickHouse.connection.insert("table", [{id: 1}], {})`) * 🔥 Added config option `json_serializer` (one of `ClickHouse::Serializer::JsonSerializer`, `ClickHouse::Serializer::JsonOjSerializer`) * 🔥 Added config option `symbolize_keys` * 🔥 Added type serialization for INSERT statements, example below: ```sql CREATE TABLE assets(visible Boolean, tags Array(Nullable(String))) ENGINE Memory ``` ```ruby # cache table schema in a class variable @schema = ClickHouse.connection.table_schema('assets') # Json each row ClickHouse.connection.insert('assets', @schema.serialize({'visible' => true, 'tags' => ['ruby']})) # Json compact ClickHouse.connection.insert('assets', columns: %w[visible tags]) do |buffer| buffer << [ @schema.serialize_column("visible", true), @schema.serialize_column("tags", ['ruby']), ] end ``` # 2.0.0 * Fixed `Bigdecimal` casting with high precision * Added nested `type casting like Array(Array(Array(Nullable(T))))` * Added `Map(T1, T2)` support * Added `Tuple(T1, T2)` support * Added support for `Faraday` v1 and v2 * Added support for `Oj` parser * Time types return `Time` class instead of `DateTime` for now # 1.6.3 * [PR](https://github.com/shlima/click_house/pull/38) Add option format for insert * [PR](https://github.com/shlima/click_house/pull/34) Support X-ClickHouse-Exception-Code header * [ISSUE](https://github.com/shlima/click_house/issues/33) Fix parameterized types parsing * Added LowCardinality DDL support * Fixed body logging with POST queries # 1.6.2 * [PR](https://github.com/shlima/click_house/pull/31) Add rows_before_limit_at_least to ResultSet * [PR](https://github.com/shlima/click_house/pull/29) Force JSON format by using "default_format" instead of modifying the query # 1.6.1 * [PR](https://github.com/shlima/click_house/pull/26) call logging middleware when an error is raised # 1.6.0 * [PR](https://github.com/shlima/click_house/pull/19) handle value returned as nil in float and integer types (case of Aggregate Function Combinators) * [PR](https://github.com/shlima/click_house/pull/18) Fix Faraday deprecation # 1.5.0 * add support for 'WITH TOTALS' modifier in response * send SQL in GET request's body [#12](https://github.com/shlima/click_house/pull/12) * add support of 'WITH TOTALS' on a resulting set # 1.4.0 * fix decimal type casting [#11](https://github.com/shlima/click_house/issues/11) # 1.3.9 * add `ClickHouse.connection.add_index`, `ClickHouse.connection.drop_index` # 1.3.8 * fix `DateTime` casting for queries like `ClickHouse.connection.select_value('select NOW()')` * fix resulting set console inspection # 1.3.7 * specify required ruby version [#10](https://github.com/shlima/click_house/issues/10) # 1.3.6 * fix ruby 2.7 warning `maybe ** should be added to the call` on `ClickHouse.connection.databases` # 1.3.5 * added `ClickHouse.connection.explain("sql")` # 1.3.4 * added `ClickHouse.type_names(nullable: false)` * fixed `connection#create_table` column definitions * `ClickHouse.add_type` now handles Nullable types automatically # 1.3.3 * fix logger typo # 1.3.2 * fix null logger for windows users # 1.3.1 * added request [headers](https://github.com/shlima/click_house/pull/8) support # 1.3.0 * added support for IPv4/IPv6 types # 1.2.7 * rubocop version bump # 1.2.6 * Datetime64 field type support [#3](https://github.com/shlima/click_house/pull/3) ================================================ FILE: Gemfile ================================================ # frozen_string_literal: true source 'https://rubygems.org' git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } # Specify your gem's dependencies in click_house.gemspec gemspec ================================================ FILE: Gemfile_faraday1 ================================================ # frozen_string_literal: true source 'https://rubygems.org' git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } # lock faraday to v1 gem 'faraday', '< 2' # Specify your gem's dependencies in click_house.gemspec gemspec ================================================ FILE: LICENCE.txt ================================================ MIT License COPYRIGHT (C) 2019 ALIAKSANDR SHYLAU Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ .PHONY: help .BUNDLE_GEMFILE:= .REQUIRE:=./spec/spec_helper help: @echo 'Available targets:' @echo ' make dockerize OR make ARGS="--build" dockerize' @echo ' make release' @echo ' ' @echo ' make faraday1 bundle' @echo ' make faraday2 bundle' @echo ' ' @echo ' make faraday1 rspec' @echo ' make faraday2 rspec' @echo ' make faraday2 oj rspec' dockerize: docker-compose up ${ARGS} release: bin/release.sh faraday1: $(eval .BUNDLE_GEMFILE=Gemfile_faraday1) faraday2: $(eval .BUNDLE_GEMFILE=Gemfile_faraday2) oj: $(eval .REQUIRE=./spec/oj_helper) bundle: BUNDLE_GEMFILE=${.BUNDLE_GEMFILE} bundle rspec: BUNDLE_GEMFILE=${.BUNDLE_GEMFILE} rspec --require ${.REQUIRE} spec ================================================ FILE: README.md ================================================ ![](./doc/logo.svg?sanitize=true) # ClickHouse Ruby driver ![CI](https://github.com/shlima/click_house/workflows/CI/badge.svg) [![Code Climate](https://codeclimate.com/github/shlima/click_house/badges/gpa.svg)](https://codeclimate.com/github/shlima/click_house) [![Gem Version](https://badge.fury.io/rb/click_house.svg)](https://badge.fury.io/rb/click_house) ```bash gem install click_house ``` A modern Ruby database driver for ClickHouse. [ClickHouse](https://clickhouse.yandex) is a high-performance column-oriented database management system developed by [Yandex](https://yandex.com/company) which operates Russia's most popular search engine. > This development was inspired by currently [unmaintainable alternative](https://github.com/archan937/clickhouse) > but rewritten and well tested ### Why use the HTTP interface and not the TCP interface? Well, the developers of ClickHouse themselves [discourage](https://github.com/yandex/ClickHouse/issues/45#issuecomment-231194134) using the TCP interface. > TCP transport is more specific, we don't want to expose details. Despite we have full compatibility of protocol of different versions of client and server, we want to keep the ability to "break" it for very old clients. And that protocol is not too clean to make a specification. Yandex uses HTTP interface for working from Java and Perl, Python and Go as well as shell scripts. # TOC * [Configuration](#configuration) * [Usage](#usage) * [Queries](#queries) * [Insert](#insert) * [Create a table](#create-a-table) * [Alter table](#alter-table) * [Type casting](#type-casting) * [Using with a connection pool](#using-with-a-connection-pool) * [Using with Rails](#using-with-rails) * [Using with ActiveRecord](#using-with-activerecord) * [Using with RSpec](#using-with-rspec) * [Development](#development) ## Configuration ```ruby ClickHouse.config do |config| config.logger = Logger.new(STDOUT) config.adapter = :net_http config.database = 'metrics' config.url = 'http://localhost:8123' config.timeout = 60 config.open_timeout = 3 config.ssl_verify = false # set to true to symbolize keys for SELECT and INSERT statements (type casting) config.symbolize_keys = false config.headers = {} # or provide connection options separately config.scheme = 'http' config.host = 'localhost' config.port = 'port' # if you use HTTP basic Auth config.username = 'user' config.password = 'password' # if you want to add settings to all queries config.global_params = { mutations_sync: 1 } # choose a ruby JSON parser (default one) config.json_parser = ClickHouse::Middleware::ParseJson # or Oj parser config.json_parser = ClickHouse::Middleware::ParseJsonOj # JSON.dump (default one) config.json_serializer = ClickHouse::Serializer::JsonSerializer # or Oj.dump config.json_serializer = ClickHouse::Serializer::JsonOjSerializer end ``` Alternative, you can assign configuration parameters via a hash ```ruby ClickHouse.config.assign(logger: Logger.new(STDOUT)) ``` Now you are able to communicate with ClickHouse: ```ruby ClickHouse.connection.ping #=> true ``` You can easily build a new raw connection and override any configuration parameter (such as database name, connection address) ```ruby @connection = ClickHouse::Connection.new(ClickHouse::Config.new(logger: Rails.logger)) @connection.ping ``` ## Usage ```ruby ClickHouse.connection.ping #=> true ClickHouse.connection.replicas_status #=> true ClickHouse.connection.databases #=> ["default", "system"] ClickHouse.connection.create_database('metrics', if_not_exists: true, engine: nil, cluster: nil) ClickHouse.connection.drop_database('metrics', if_exists: true, cluster: nil) ClickHouse.connection.tables #=> ["visits"] ClickHouse.connection.describe_table('visits') #=> [{"name"=>"id", "type"=>"FixedString(16)", "default_type"=>""}] ClickHouse.connection.table_exists?('visits', temporary: nil) #=> true ClickHouse.connection.drop_table('visits', if_exists: true, temporary: nil, cluster: nil) ClickHouse.connection.create_table(*) # see section ClickHouse.connection.truncate_table('name', if_exists: true, cluster: nil) ClickHouse.connection.truncate_tables(['table_1', 'table_2'], if_exists: true, cluster: nil) ClickHouse.connection.truncate_tables # will truncate all tables in database ClickHouse.connection.rename_table('old_name', 'new_name', cluster: nil) ClickHouse.connection.rename_table(%w[table_1 table_2], %w[new_1 new_2], cluster: nil) ClickHouse.connection.alter_table('table', 'DROP COLUMN user_id', cluster: nil) ClickHouse.connection.add_index('table', 'ix', 'has(b, a)', type: 'minmax', granularity: 2, cluster: nil) ClickHouse.connection.drop_index('table', 'ix', cluster: nil) ClickHouse.connection.select_all('SELECT * FROM visits') ClickHouse.connection.select_one('SELECT * FROM visits LIMIT 1') ClickHouse.connection.select_value('SELECT ip FROM visits LIMIT 1') ClickHouse.connection.explain('SELECT * FROM visits CROSS JOIN visits') ``` ## Queries ### Select All Select all type-casted result set ```ruby @result = ClickHouse.connection.select_all('SELECT * FROM visits') # all enumerable methods are delegated like #each, #map, #select etc # results of #to_a is TYPE CASTED @result.to_a #=> [{"date"=>#, "id"=>1}] # raw results (WITHOUT type casting) # much faster if selecting a large amount of data @result.data #=> [{"date"=>"2000-01-01", "id"=>1}, {"date"=>"2000-01-02", "id"=>2}] # you can access raw data @result.meta #=> [{"name"=>"date", "type"=>"Date"}, {"name"=>"id", "type"=>"UInt32"}] @result.statistics #=> {"elapsed"=>0.0002271, "rows_read"=>2, "bytes_read"=>12} @result.summary #=> ClickHouse::Response::Summary @result.headers #=> {"x-clickhouse-query-id"=>"9bf5f604-31fc-4eff-a4b5-277f2c71d199"} @result.types #=> [Hash] ``` ### Select Value Select value returns exactly one type-casted value ```ruby ClickHouse.connection.select_value('SELECT COUNT(*) from visits') #=> 0 ClickHouse.connection.select_value("SELECT toDate('2019-01-01')") #=> # ClickHouse.connection.select_value("SELECT toDateOrZero(NULL)") #=> nil ``` ### Select One Returns a record hash with the column names as keys and column values as values. ```ruby ClickHouse.connection.select_one('SELECT date, SUM(id) AS sum FROM visits GROUP BY date') #=> {"date"=>#, "sum"=>1} ``` ### Execute Raw SQL By default, gem provides parser for `JSON` and `CSV` response formats. Type conversion available for the `JSON`. ```ruby # format not specified response = ClickHouse.connection.execute <<~SQL SELECT count(*) AS counter FROM rspec SQL response.body #=> "2\n" # JSON response = ClickHouse.connection.execute <<~SQL SELECT count(*) AS counter FROM rspec FORMAT JSON SQL response.body #=> {"meta"=>[{"name"=>"counter", "type"=>"UInt64"}], "data"=>[{"counter"=>"2"}], "rows"=>1, "statistics"=>{"elapsed"=>0.0002412, "rows_read"=>2, "bytes_read"=>4}} # CSV response = ClickHouse.connection.execute <<~SQL SELECT count(*) AS counter FROM rspec FORMAT CSV SQL response.body #=> [["2"]] # You may use any format supported by ClickHouse response = ClickHouse.connection.execute <<~SQL SELECT count(*) AS counter FROM rspec FORMAT RowBinary SQL response.body #=> "\u0002\u0000\u0000\u0000\u0000\u0000\u0000\u0000" ``` ## Insert When column names and values are transferred separately, data sends to the server using `JSONCompactEachRow` format by default. ```ruby ClickHouse.connection.insert('table', columns: %i[id name]) do |buffer| buffer << [1, 'Mercury'] buffer << [2, 'Venus'] end # or ClickHouse.connection.insert('table', columns: %i[id name], values: [[1, 'Mercury'], [2, 'Venus']]) ``` When rows are passed as an Array or a Hash, data sends to the server using `JSONEachRow` format by default. ```ruby ClickHouse.connection.insert('table', [{ name: 'Sun', id: 1 }, { name: 'Moon', id: 2 }]) # or ClickHouse.connection.insert('table', { name: 'Sun', id: 1 }) # for ruby < 3.0 provide an extra argument ClickHouse.connection.insert('table', { name: 'Sun', id: 1 }, {}) # or ClickHouse.connection.insert('table') do |buffer| buffer << { name: 'Sun', id: 1 } buffer << { name: 'Moon', id: 2 } end ``` Sometimes it's needed to use other format than `JSONEachRow` For example if you want to send BigDecimal's you could use `JSONStringsEachRow` format so string representation of `BigDecimal` will be parsed: ```ruby ClickHouse.connection.insert('table', { name: 'Sun', id: '1' }, format: 'JSONStringsEachRow') # or ClickHouse.connection.insert_rows('table', { name: 'Sun', id: '1' }, format: 'JSONStringsEachRow') # or ClickHouse.connection.insert_compact('table', columns: %w[name id], values: %w[Sun 1], format: 'JSONCompactStringsEachRow') ``` See the [type casting](#type-casting) section to insert the data in a proper way. ## Create a table ### Create table using DSL ```ruby ClickHouse.connection.create_table('visits', if_not_exists: true, engine: 'MergeTree(date, (year, date), 8192)') do |t| t.FixedString :id, 16 t.UInt16 :year, low_cardinality: true t.Date :date t.DateTime :time, 'UTC' t.Decimal :money, 5, 4 t.String :event t.UInt32 :user_id t.IPv4 :ipv4 t.IPv6 :ipv6 end ``` ### Create nullable columns ```ruby ClickHouse.connection.create_table('visits', engine: 'TinyLog') do |t| t.UInt16 :id, 16, nullable: true end ``` ### Set column options ```ruby ClickHouse.connection.create_table('visits', engine: 'MergeTree(date, (year, date), 8192)') do |t| t.UInt16 :year t.Date :date t.UInt16 :id, 16, default: 0, ttl: 'date + INTERVAL 1 DAY' end ``` ### Define column with custom SQL ```ruby ClickHouse.connection.create_table('visits', engine: 'TinyLog') do |t| t << "vendor Enum('microsoft' = 1, 'apple' = 2)" t << "tags Array(String)" end ``` ### Define nested structures ```ruby ClickHouse.connection.create_table('visits', engine: 'TinyLog') do |t| t.UInt8 :id t.Nested :json do |n| n.UInt8 :cid n.Date :created_at n.Date :updated_at end end ``` ### Set table options ```ruby ClickHouse.connection.create_table('visits', order: 'year', ttl: 'date + INTERVAL 1 DAY', sample: 'year', settings: 'index_granularity=8192', primary_key: 'year', engine: 'MergeTree') do |t| t.UInt16 :year t.Date :date end ``` ### Create table with raw SQL ```ruby ClickHouse.connection.execute <<~SQL CREATE TABLE visits(int Nullable(Int8), date Nullable(Date)) ENGINE TinyLog SQL ``` ## Alter table ### Alter table with DSL ```ruby ClickHouse.connection.add_column('table', 'column_name', :UInt64, default: nil, if_not_exists: nil, after: nil, cluster: nil) ClickHouse.connection.drop_column('table', 'column_name', if_exists: nil, cluster: nil) ClickHouse.connection.clear_column('table', 'column_name', partition: 'partition_name', if_exists: nil, cluster: nil) ClickHouse.connection.modify_column('table', 'column_name', type: :UInt64, default: nil, if_exists: false, cluster: nil) ``` ### Alter table with SQL ```ruby # By SQL in argument ClickHouse.connection.alter_table('table', 'DROP COLUMN user_id', cluster: nil) # By SQL in a block ClickHouse.connection.alter_table('table', cluster: nil) do <<~SQL MOVE PART '20190301_14343_16206_438' TO VOLUME 'slow' SQL end ``` ## Type casting By default gem provides all necessary type casting, but you may overwrite or define your own logic. if you need to redefine all built-in types with your implementation, just clear the default type system: ```ruby ClickHouse.types.clear ClickHouse.types # => {} ClickHouse.types.default #=> # ``` Type casting works automatically when fetching data, when inserting data, you must serialize the types yourself ```sql CREATE TABLE assets(visible Boolean, tags Array(Nullable(String))) ENGINE Memory ``` ```ruby # cache table schema in a class variable @schema = ClickHouse.connection.table_schema('assets') # Json each row ClickHouse.connection.insert('assets', @schema.serialize({'visible' => true, 'tags' => ['ruby']})) # Json compact ClickHouse.connection.insert('assets', columns: %w[visible tags]) do |buffer| buffer << [ @schema.serialize_column("visible", true), @schema.serialize_column("tags", ['ruby']), ] end ``` ## Using with a connection pool ```ruby require 'connection_pool' ClickHouse.connection = ConnectionPool.new(size: 2) do ClickHouse::Connection.new(ClickHouse::Config.new(url: 'http://replica.example.com')) end ClickHouse.connection.with do |conn| conn.tables end ``` ## Using with Rails ```yml # config/click_house.yml default: &default url: http://localhost:8123 timeout: 60 open_timeout: 3 development: database: ecliptic_development <<: *default test: database: ecliptic_test <<: *default production: <<: *default database: ecliptic_production ``` ```ruby # config/initializers/click_house.rb ClickHouse.config do |config| config.logger = Rails.logger config.assign(Rails.application.config_for('click_house')) end ``` ```ruby # lib/tasks/click_house.rake namespace :click_house do task prepare: :environment do @environments = Rails.env.development? ? %w[development test] : [Rails.env] end task drop: :prepare do @environments.each do |env| config = ClickHouse.config.clone.assign(Rails.application.config_for('click_house', env: env)) connection = ClickHouse::Connection.new(config) connection.drop_database(config.database, if_exists: true) end end task create: :prepare do @environments.each do |env| config = ClickHouse.config.clone.assign(Rails.application.config_for('click_house', env: env)) connection = ClickHouse::Connection.new(config) connection.create_database(config.database, if_not_exists: true) end end end ``` Prepare the ClickHouse database: ```bash rake click_house:drop click_house:create ``` If your are using SQL Database in Rails, you can manage ClickHouse migrations using `ActiveRecord::Migration` mechanism ```ruby class CreateAdvertVisits < ActiveRecord::Migration[6.0] def up ClickHouse.connection.create_table('visits', engine: 'MergeTree(date, (account_id, advert_id), 512)') do |t| t.UInt16 :account_id t.UInt16 :user_id t.Date :date end end def down ClickHouse.connection.drop_table('visits') end end ``` ## Using with ActiveRecord if you use `ActiveRecord`, you can use the ORM query builder by using fake models (empty tables must be present in the SQL database `create_table :visits`) ```ruby class ClickHouseRecord < ActiveRecord::Base self.abstract_class = true class << self def agent ClickHouse.connection end def insert(*argv, &block) agent.insert(table_name, *argv, &block) end def select_one agent.select_one(current_scope.to_sql) end def select_value agent.select_value(current_scope.to_sql) end def select_all agent.select_all(current_scope.to_sql) end def explain agent.explain(current_scope.to_sql) end end end ```` ````ruby # FAKE MODEL FOR ClickHouse class Visit < ClickHouseRecord scope :with_os, -> { where.not(os_family_id: nil) } end Visit.with_os.select('COUNT(*) as counter').group(:ipv4).select_all #=> [{ 'ipv4' => 1455869, 'counter' => 104 }] Visit.with_os.select('COUNT(*)').select_value #=> 20_345_678 Visit.where(user_id: 1).select_one #=> { 'ipv4' => 1455869, 'user_id' => 1 } ```` ## Using with RSpec You can clear the data table before each test with RSpec ```ruby RSpec.configure do |config| config.before(:each, truncate_click_house: true) do ClickHouse.connection.truncate_tables end end ``` ```ruby RSpec.describe Api::MetricsCountroller, truncate_click_house: true do it { } it { } end ``` ## Development ```bash make dockerize rspec rubocop ``` ================================================ FILE: Rakefile ================================================ # frozen_string_literal: true require 'bundler/gem_tasks' require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) task default: :spec ================================================ FILE: bin/console ================================================ #!/usr/bin/env ruby require 'bundler/setup' require 'click_house' require 'pry' ClickHouse.config do |config| config.logger = Logger.new(STDOUT) end Pry.start ================================================ FILE: bin/release.sh ================================================ #!/usr/bin/env bash rm ./*.gem gem build click_house.gemspec gem push click_house-* ================================================ FILE: bin/setup ================================================ #!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' set -vx bundle install # Do any other automated setup that you need to do here ================================================ FILE: click_house.gemspec ================================================ lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'click_house/version' Gem::Specification.new do |spec| spec.name = 'click_house' spec.version = ClickHouse::VERSION spec.authors = ['Aliaksandr Shylau'] spec.email = ['alex.shilov.by@gmail.com'] spec.summary = 'Modern Ruby database driver for ClickHouse' spec.description = 'Yandex ClickHouse database interface for Ruby' spec.homepage = 'https://github.com/shlima/click_house' spec.required_ruby_version = '>= 2.7.0' # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } end spec.require_paths = ['lib'] spec.add_dependency 'faraday', '>= 1.7', '< 3' spec.add_dependency 'activesupport' spec.add_development_dependency 'bundler' spec.add_development_dependency 'rake' spec.add_development_dependency 'oj' spec.add_development_dependency 'rspec' spec.add_development_dependency 'pry' spec.add_development_dependency 'rubocop' spec.add_development_dependency 'rubocop-performance' end ================================================ FILE: docker-compose.yml ================================================ version: '3.5' services: clickhouse: image: clickhouse/clickhouse-server:22.9 ports: - "8123:8123" - "9000:9000" - "9009:9009" ulimits: nproc: 65535 nofile: soft: 262144 hard: 262144 volumes: - ./tmp/clickhouse-data:/opt/clickhouse/data networks: - default networks: default: ================================================ FILE: lib/click_house/ast/parser.rb ================================================ # frozen_string_literal: true require 'stringio' module ClickHouse module Ast class Parser OPEN = '(' CLOSED = ')' COMMA = ',' SPACE = ' ' attr_reader :input # @param input [String] def initialize(input) @input = input end # @refs https://clickhouse.com/docs/en/sql-reference/data-types/ # Map(String, Decimal(10, 5)) # Array(Array(Array(Array(Nullable(Int, String))))) def parse ticker = Ticker.new control = false input.each_char do |char| # cases like (1, 3) next if control && char == SPACE case char when OPEN control = true ticker.open when CLOSED control = true ticker.close when COMMA control = true ticker.comma else control = false ticker.char(char) end end # if a single type like "Int" ticker.current.name! unless control ticker.current end end end end ================================================ FILE: lib/click_house/ast/statement.rb ================================================ # frozen_string_literal: true require 'stringio' module ClickHouse module Ast class Statement PLACEHOLDER_S = '%s' PLACEHOLDER_D = '%d' DIGIT_RE = /\A\d+\Z/.freeze attr_reader :name attr_accessor :caster def initialize(name: '') @buffer = '' @name = name end # @param value [String] def print(value) @buffer = "#{@buffer}#{value}" end def name! @name = @buffer @buffer = '' end def argument! add_argument(Statement.new(name: @buffer)) @buffer = '' end # @param st [Statement] def add_argument(st) arguments.push(st) end # @param other [Statement] def merge(other) if other.named? add_argument(other) else @arguments = arguments.concat(other.arguments) end end def named? !@name.empty? end def buffer? !@buffer.empty? end # @return [Array] def arguments @arguments ||= [] end # @return [Array] # cached argument values to increase the casting perfomance def argument_values @argument_values ||= arguments.map(&:value) end def argument_first! # TODO: raise an error if multiple arguments @argument_first ||= arguments.first end def placeholder return @placeholder if defined?(@placeholder) @placeholder = digit? ? PLACEHOLDER_D : PLACEHOLDER_S end def digit? name.match?(DIGIT_RE) end def value @value ||= case placeholder when PLACEHOLDER_D Integer(name) when PLACEHOLDER_S # remove leading and trailing quotes name[1..-2] else raise "unknown value extractor for <#{placeholder}>" end end def to_s out = StringIO.new out.print(name.empty? ? 'NO_NAME' : name) out.print("<#{@buffer}>") unless @buffer.empty? if arguments.any? out.print("(#{arguments.join(',')})") end out.string end end end end ================================================ FILE: lib/click_house/ast/ticker.rb ================================================ # frozen_string_literal: true require 'stringio' module ClickHouse module Ast class Ticker attr_reader :root, :current def initialize @current = Statement.new end def open current.name! opened.push(current) @current = Statement.new end def comma current.argument! if current.buffer? opened.last.merge(current) @current = Statement.new end def close current.argument! unless current.named? opened.last.merge(current) @current = opened.pop end # @param char [String] def char(char) current.print(char) end def opened @opened ||= [] end end end end ================================================ FILE: lib/click_house/ast.rb ================================================ # frozen_string_literal: true module ClickHouse module Ast autoload :Statement, 'click_house/ast/statement' autoload :Ticker, 'click_house/ast/ticker' autoload :Parser, 'click_house/ast/parser' end end ================================================ FILE: lib/click_house/benchmark/casting.rb ================================================ # frozen_string_literal: true require 'bundler/setup' require 'benchmark' require 'pry' require_relative '../../click_house' ClickHouse.config.json_serializer = ClickHouse::Serializer::JsonOjSerializer ClickHouse.config.json_parser = ClickHouse::Middleware::ParseJsonOj ClickHouse.connection.drop_table('benchmark', if_exists: true) ClickHouse.connection.execute <<~SQL CREATE TABLE benchmark( int Nullable(Int8), date Nullable(Date), array Array(String), map Map(String, IPv4) ) ENGINE Memory SQL INPUT = Array.new(200_000, { 'int' => 21341234, 'date' => Date.new(2022, 1, 1), 'array' => ['foo'], 'map' => {'ip' => IPAddr.new('127.0.0.1')} }) Benchmark.bm do |x| x.report('insert: no casting') do ClickHouse.connection.insert('benchmark', INPUT) end x.report('insert: with casting') do schema = ClickHouse.connection.table_schema('benchmark') ClickHouse.connection.insert('benchmark', schema.serialize(INPUT)) end x.report('select: no casting') do ClickHouse.connection.select_all('SELECT * FROM benchmark').data end x.report('select: with casting') do ClickHouse.connection.select_all('SELECT * FROM benchmark').to_a end end ================================================ FILE: lib/click_house/benchmark/map_join.rb ================================================ # frozen_string_literal: true require 'benchmark' require 'stringio' INPUT = Array.new(5_000_000, 'foo bar') Benchmark.bm do |x| x.report('map.join') do INPUT.map(&:to_s).join("\n") end x.report('StringIO') do out = StringIO.new INPUT.each do |value| out << "#{value}\n" end out.string end end ================================================ FILE: lib/click_house/config.rb ================================================ # frozen_string_literal: true module ClickHouse class Config DEFAULTS = { adapter: Faraday.default_adapter, url: nil, scheme: 'http', host: 'localhost', port: '8123', logger: nil, database: nil, username: nil, password: nil, timeout: nil, open_timeout: nil, ssl_verify: false, headers: {}, global_params: {}, json_parser: ClickHouse::Middleware::ParseJson, json_serializer: ClickHouse::Serializer::JsonSerializer, oj_dump_options: { mode: :compat # to be able to dump improper JSON like {1 => 2} }, oj_load_options: { mode: :custom, allow_blank: true, bigdecimal_as_decimal: false, # dump BigDecimal as a String bigdecimal_load: :bigdecimal, # convert all decimal numbers to BigDecimal }, json_load_options: { decimal_class: BigDecimal, }, # should be after json load options symbolize_keys: false, }.freeze attr_accessor :adapter attr_accessor :logger attr_accessor :scheme attr_accessor :host attr_accessor :port attr_accessor :database attr_accessor :url attr_accessor :username attr_accessor :password attr_accessor :timeout attr_accessor :open_timeout attr_accessor :ssl_verify attr_accessor :headers attr_accessor :global_params attr_accessor :oj_load_options attr_accessor :json_load_options attr_accessor :json_parser # response middleware attr_accessor :oj_dump_options attr_accessor :json_serializer # [ClickHouse::Serializer::Base] attr_accessor :symbolize_keys # [NilClass, Boolean] def initialize(params = {}) assign(DEFAULTS.merge(params)) yield(self) if block_given? end # @return [self] def assign(params = {}) params.each { |k, v| public_send("#{k}=", v) } self end def auth? !username.nil? || !password.nil? end def logger! @logger || null_logger end def url! @url || "#{scheme}://#{host}:#{port}" end def null_logger @null_logger ||= Logger.new(IO::NULL) end # @param klass [ClickHouse::Serializer::Base] def json_serializer=(klass) @json_serializer = klass.new(self) end def symbolize_keys=(value) bool = value ? true : false # merge to be able to clone a config # prevent overriding default values self.oj_load_options = oj_load_options.merge(symbol_keys: bool) self.json_load_options = json_load_options.merge(symbolize_names: bool) @symbolize_keys = bool end # @param name [Symbol, String] def key(name) symbolize_keys ? name.to_sym : name.to_s end end end ================================================ FILE: lib/click_house/connection.rb ================================================ # frozen_string_literal: true module ClickHouse class Connection include Extend::ConnectionHealthy include Extend::ConnectionDatabase include Extend::ConnectionTable include Extend::ConnectionSelective include Extend::ConnectionInserting include Extend::ConnectionAltering include Extend::ConnectionExplaining attr_reader :config # @param [Config] def initialize(config) @config = config end def execute(query, body = nil, database: config.database, params: {}) post(body, query: { query: query }, database: database, params: config.global_params.merge(params)) end # @param path [String] Clickhouse HTTP endpoint, e.g. /ping, /replica_status # @param body [String] SQL to run # @param database [String|NilClass] database to use, nil to skip # @param query [Hash] other CH settings to send through params, e.g. max_rows_to_read=1 # @example get(body: 'select number from system.numbers limit 100', query: { max_rows_to_read: 10 }) # @return [Faraday::Response] def get(path = '/', body: '', query: {}, database: config.database) # backward compatibility since # https://github.com/shlima/click_house/pull/12/files#diff-9c6f3f06d3b575731eae4b6b95ddbcdcc20452c432b8f6e87a3a8e8645818107R24 if query.is_a?(String) query = { query: query } config.logger!.warn('since v1.4.0 use connection.get(body: "SELECT 1") instead of connection.get(query: "SELECT 1")') end transport.get(path) do |conn| conn.params = query.merge(database: database).compact conn.params[:send_progress_in_http_headers] = 1 unless body.empty? conn.body = body end end def post(body = nil, query: {}, database: config.database, params: {}) transport.post(compose('/', query.merge(database: database, **params)), body) end # transport should work the same both with Faraday v1 and Faraday v2 # rubocop:disable Metrics/AbcSize def transport @transport ||= Faraday.new(config.url!) do |conn| conn.options.timeout = config.timeout conn.options.open_timeout = config.open_timeout conn.headers = config.headers conn.ssl.verify = config.ssl_verify if config.auth? if faraday_v1? conn.request :basic_auth, config.username, config.password else conn.request :authorization, :basic, config.username, config.password end end conn.response Middleware::RaiseError conn.response Middleware::Logging, logger: config.logger! conn.response Middleware::SummaryMiddleware, options: { config: config } # should be after logger conn.response config.json_parser, content_type: %r{application/json}, options: { config: config } conn.response Middleware::ParseCsv, content_type: %r{text/csv}, options: { config: config } conn.adapter config.adapter end end # rubocop:enable Metrics/AbcSize def compose(path, query = {}) # without "DB::Exception: Empty query" error will occur "#{path}?#{URI.encode_www_form({ send_progress_in_http_headers: 1 }.merge(query).compact)}" end # @return [Boolean] def faraday_v1? Faraday::VERSION.start_with?('1') end end end ================================================ FILE: lib/click_house/definition/column.rb ================================================ # frozen_string_literal: true module ClickHouse module Definition class Column attr_accessor :name attr_accessor :type attr_accessor :nullable attr_accessor :low_cardinality attr_accessor :extensions attr_accessor :default attr_accessor :materialized attr_accessor :ttl def initialize(params = {}) params.each { |k, v| public_send("#{k}=", v) } yield(self) if block_given? end def to_s type = extension_type type = "Nullable(#{type})" if nullable type = "LowCardinality(#{type})" if low_cardinality "#{name} #{type} #{opts}" end def opts options = { DEFAULT: Util::Statement.ensure(default, default), MATERIALIZED: Util::Statement.ensure(materialized, materialized), TTL: Util::Statement.ensure(ttl, ttl) }.compact result = options.each_with_object([]) do |(key, value), object| object << "#{key} #{value}" end result.join(' ') end def extension_type extensions.nil? ? type : format(type, *extensions) rescue TypeError, ArgumentError raise StandardError, "please provide extensions for <#{type}>" end end end end ================================================ FILE: lib/click_house/definition/column_set.rb ================================================ # frozen_string_literal: true module ClickHouse module Definition class ColumnSet TYPES = ClickHouse.types.each_with_object([]) do |(name, type), object| object << name.sub('%s', "'%s'") if type.ddl? end class << self # @input "DateTime('%s')" # @output "DateTime" def method_name_for_type(type) type.sub(/\(.+/, '') end end TYPES.each do |type| method_name = method_name_for_type(type) # t.Decimal :customer_id, nullable: true, default: '' # t.Decimal :money, 1, 2, nullable: true, default: '' class_eval <<-METHODS, __FILE__, __LINE__ + 1 def #{method_name}(*definition) name = definition[0] extentions = [] options = {} extensions = Array(definition[1..-1]).each do |el| el.is_a?(Hash) ? options.merge!(el) : extentions.push(el) end columns << Column.new(type: "#{type}", name: name, extensions: extensions, **options) end METHODS end def initialize yield(self) if block_given? end def columns @columns ||= [] end def to_s <<~SQL ( #{columns.map(&:to_s).join(', ')} ) SQL end # @example # t.Nested :json do |n| # n.UInt8 :city_id # end def nested(name, &block) columns << "#{name} Nested #{ColumnSet.new(&block)}" end alias_method :Nested, :nested def push(sql) columns << sql end alias_method :<<, :push end end end __END__ data = ClickHouse::Definition::ColumnSet.new do |t| t << "words Enum('hello' = 1, 'world' = 2)" end puts data.to_s data = ClickHouse::Definition::ColumnSet.new do |t| t.Decimal :money t.Float32 :client_id, default: 0 t.Float32 :city_id, default: 0, nullable: true t.Nested :json do |n| n.Date :created_at n.Date :updated_at end t << "CUSTOM SQL" end ================================================ FILE: lib/click_house/definition.rb ================================================ # frozen_string_literal: true module ClickHouse module Definition autoload :Column, 'click_house/definition/column' autoload :ColumnSet, 'click_house/definition/column_set' end end ================================================ FILE: lib/click_house/errors.rb ================================================ # frozen_string_literal: true module ClickHouse Error = Class.new(StandardError) NetworkException = Class.new(Error) DbException = Class.new(Error) StatementException = Class.new(Error) SerializeError = Class.new(Error) end ================================================ FILE: lib/click_house/extend/configurable.rb ================================================ # frozen_string_literal: true module ClickHouse module Extend module Configurable def config(&block) yield(@config) if defined?(@config) && block @config ||= Config.new(&block) end end end end ================================================ FILE: lib/click_house/extend/connectible.rb ================================================ # frozen_string_literal: true module ClickHouse module Extend module Connectible def connection=(connection) @connection = connection end def connection @connection ||= Connection.new(config.clone) end end end end ================================================ FILE: lib/click_house/extend/connection_altering.rb ================================================ # frozen_string_literal: true # rubocop:disable Metrics/ParameterLists module ClickHouse module Extend module ConnectionAltering def add_column(table, name, type, default: nil, if_not_exists: false, after: nil, cluster: nil) sql = 'ADD COLUMN %s %s %s %s %s' pattern = { name: name, exists: Util::Statement.ensure(if_not_exists, 'IF NOT EXISTS'), type: type, default: Util::Statement.ensure(default, "DEFAULT #{default}"), after: Util::Statement.ensure(after, "AFTER #{after}") } alter_table(table, format(sql, pattern), cluster: cluster) end def drop_column(table, name, if_exists: false, cluster: nil) sql = 'DROP COLUMN %s %s' pattern = { name: name, exists: Util::Statement.ensure(if_exists, 'IF EXISTS') } alter_table(table, format(sql, pattern), cluster: cluster) end def clear_column(table, name, partition:, if_exists: false, cluster: nil) sql = 'CLEAR COLUMN %s %s %s' pattern = { name: name, exists: Util::Statement.ensure(if_exists, 'IF EXISTS'), partition: "IN PARTITION #{partition}" } alter_table(table, format(sql, pattern), cluster: cluster) end def modify_column(table, name, type: nil, default: nil, if_exists: false, cluster: nil) sql = 'MODIFY COLUMN %s %s %s %s' pattern = { name: name, type: type, exists: Util::Statement.ensure(if_exists, 'IF EXISTS'), default: Util::Statement.ensure(default, "DEFAULT #{default}") } alter_table(table, format(sql, pattern), cluster: cluster) end def alter_table(name, sql = nil, cluster: nil) template = 'ALTER TABLE %s %s %s' sql = yield(sql) if sql.nil? pattern = { name: name, sql: sql, cluster: Util::Statement.ensure(cluster, "ON CLUSTER #{cluster}"), } execute(format(template, pattern)).success? end def add_index( table_name, name, expression, type:, granularity: nil, after: nil, cluster: nil ) template = 'ADD INDEX %s %s TYPE %s GRANULARITY %d %s' pattern = { name: name, expression: expression, type: type, granularity: granularity, after: Util::Statement.ensure(after, "AFTER #{after}"), } alter_table(table_name, format(template, pattern), cluster: cluster) end def drop_index(table_name, name, cluster: nil) alter_table(table_name, <<~SQL, cluster: cluster) DROP INDEX #{name} SQL end end end end # rubocop:enable Metrics/ParameterLists ================================================ FILE: lib/click_house/extend/connection_database.rb ================================================ # frozen_string_literal: true module ClickHouse module Extend module ConnectionDatabase # @return [Array] def databases Array(execute('SHOW DATABASES FORMAT CSV', database: nil).body).tap(&:flatten!) end def create_database(name, if_not_exists: false, cluster: nil, engine: nil) sql = 'CREATE DATABASE %s %s %s %s' pattern = { name: name, exists: Util::Statement.ensure(if_not_exists, 'IF NOT EXISTS'), cluster: Util::Statement.ensure(cluster, "ON CLUSTER #{cluster}"), engine: Util::Statement.ensure(engine, "ENGINE = #{engine}") } execute(format(sql, pattern), database: nil).success? end def drop_database(name, if_exists: false, cluster: nil) sql = 'DROP DATABASE %s %s %s' pattern = { name: name, exists: Util::Statement.ensure(if_exists, 'IF EXISTS'), cluster: Util::Statement.ensure(cluster, "ON CLUSTER #{cluster}"), } execute(format(sql, pattern), database: nil).success? end end end end ================================================ FILE: lib/click_house/extend/connection_explaining.rb ================================================ # frozen_string_literal: true module ClickHouse module Extend module ConnectionExplaining EXPLAIN = 'EXPLAIN' EXPLAIN_RE = /\A(\s*#{EXPLAIN})/io.freeze # @return String def explain(sql, io: StringIO.new) res = execute("#{EXPLAIN} #{sql.gsub(EXPLAIN_RE, '')}") io.puts(res.body) io.string end end end end ================================================ FILE: lib/click_house/extend/connection_healthy.rb ================================================ # frozen_string_literal: true module ClickHouse module Extend module ConnectionHealthy def ping get('/ping', database: nil).success? end def replicas_status get('/replicas_status', database: nil).success? end end end end ================================================ FILE: lib/click_house/extend/connection_inserting.rb ================================================ # frozen_string_literal: true module ClickHouse module Extend module ConnectionInserting DEFAULT_JSON_EACH_ROW_FORMAT = 'JSONEachRow' DEFAULT_JSON_COMPACT_EACH_ROW_FORMAT = 'JSONCompactEachRow' # @return [Boolean] # # == Example with a block # insert('rspec', columns: %i[name id]) do |buffer| # buffer << ['Sun', 1] # buffer << ['Moon', 2] # end # # @return [Response::Execution] # @param body [Array, Hash] # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity def insert(table, body = [], **opts) # In Ruby < 3.0, if the last argument is a hash, and the method being called # accepts keyword arguments, then it is always converted to keyword arguments. columns = opts.fetch(:columns, []) values = opts.fetch(:values, []) format = opts.fetch(:format, nil) yield(body) if block_given? # values: [{id: 1}] if values.any? && columns.empty? return insert_rows(table, values, format: format) end # body: [{id: 1}] if body.any? && columns.empty? return insert_rows(table, body, format: format) end # body: [1], columns: ["id"] if body.any? && columns.any? return insert_compact(table, columns: columns, values: body, format: format) end # columns: ["id"], values: [[1]] if columns.any? && values.any? return insert_compact(table, columns: columns, values: values, format: format) end Response::Factory.empty_exec(config) end # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity # @param table [String] # @param body [Array, Hash] # @param format [String] # @return [Response::Execution] # # Sometimes it's needed to use other format than JSONEachRow # For example if you want to send BigDecimal's you could use # JSONStringsEachRow format so string representation of BigDecimal will be parsed def insert_rows(table, body, format: nil) format ||= DEFAULT_JSON_EACH_ROW_FORMAT case body when Hash Response::Factory.exec(execute("INSERT INTO #{table} FORMAT #{format}", config.json_serializer.dump(body))) when Array Response::Factory.exec(execute("INSERT INTO #{table} FORMAT #{format}", config.json_serializer.dump_each_row(body))) else raise ArgumentError, "unknown body class <#{body.class}>" end end # @return [Response::Execution] def insert_compact(table, columns: [], values: [], format: nil) format ||= DEFAULT_JSON_COMPACT_EACH_ROW_FORMAT yield(values) if block_given? response = execute("INSERT INTO #{table} (#{columns.join(',')}) FORMAT #{format}", config.json_serializer.dump_each_row(values)) Response::Factory.exec(response) end end end end ================================================ FILE: lib/click_house/extend/connection_selective.rb ================================================ # frozen_string_literal: true module ClickHouse module Extend module ConnectionSelective # @return [ResultSet] def select_all(sql) response = get(body: sql, query: { default_format: 'JSON' }) Response::Factory.response(response, config) end def select_value(sql) response = get(body: sql, query: { default_format: 'JSON' }) got = Response::Factory.response(response, config).first case got when Hash Array(got).dig(0, -1) # get a value of a first key for JSON format when Array got[0] # for CSV format else got # for RowBinary format end end def select_one(sql) response = get(body: sql, query: { default_format: 'JSON' }) Response::Factory.response(response, config).first end end end end ================================================ FILE: lib/click_house/extend/connection_table.rb ================================================ # frozen_string_literal: true module ClickHouse module Extend module ConnectionTable # @return [Array] def tables Array(execute('SHOW TABLES FORMAT CSV').body).tap(&:flatten!) end # @return [ResultSet] def describe_table(name) Response::Factory.response(execute("DESCRIBE TABLE #{name} FORMAT JSON"), config) end # @return [ResultSet] def table_schema(name) Response::Factory.response(execute("SELECT * FROM #{name} WHERE 1=0 FORMAT JSON"), config) end # @return [Boolean] def table_exists?(name, temporary: false) sql = 'EXISTS %s TABLE %s FORMAT CSV' pattern = { name: name, temporary: Util::Statement.ensure(temporary, 'TEMPORARY') } Type::BooleanType.new.cast(execute(format(sql, pattern)).body.dig(0, 0)) end def drop_table(name, temporary: false, if_exists: false, cluster: nil) sql = 'DROP %s TABLE %s %s %s' pattern = { name: name, temporary: Util::Statement.ensure(temporary, 'TEMPORARY'), exists: Util::Statement.ensure(if_exists, 'IF EXISTS'), cluster: Util::Statement.ensure(cluster, "ON CLUSTER #{cluster}"), } execute(format(sql, pattern)).success? end # rubocop:disable Metrics/ParameterLists def create_table( name, if_not_exists: false, cluster: nil, partition: nil, order: nil, primary_key: nil, sample: nil, ttl: nil, settings: nil, engine:, &block ) sql = <<~SQL CREATE TABLE %s %s %s %s %s %s %s %s %s %s %s SQL definition = ClickHouse::Definition::ColumnSet.new(&block) pattern = { name: name, exists: Util::Statement.ensure(if_not_exists, 'IF NOT EXISTS'), definition: definition.to_s, cluster: Util::Statement.ensure(cluster, "ON CLUSTER #{cluster}"), partition: Util::Statement.ensure(partition, "PARTITION BY #{partition}"), order: Util::Statement.ensure(order, "ORDER BY #{order}"), primary_key: Util::Statement.ensure(primary_key, "PRIMARY KEY #{primary_key}"), sample: Util::Statement.ensure(sample, "SAMPLE BY #{sample}"), ttl: Util::Statement.ensure(ttl, "TTL #{ttl}"), settings: Util::Statement.ensure(settings, "SETTINGS #{settings}"), engine: Util::Statement.ensure(engine, "ENGINE = #{engine}") } execute(format(sql, pattern)).success? end # rubocop:enable Metrics/ParameterLists def truncate_table(name, if_exists: false, cluster: nil) sql = 'TRUNCATE TABLE %s %s %s' pattern = { name: name, exists: Util::Statement.ensure(if_exists, 'IF EXISTS'), cluster: Util::Statement.ensure(cluster, "ON CLUSTER #{cluster}") } execute(format(sql, pattern)).success? end def truncate_tables(names = tables, *argv) Array(names).each { |name| truncate_table(name, *argv) } end def rename_table(from, to, cluster: nil) from = Array(from) to = Array(to) unless from.length == to.length raise StatementException, ' tables length should equal length' end sql = <<~SQL RENAME TABLE %s %s SQL pattern = { names: from.zip(to).map { |a| a.join(' TO ') }.join(', '), cluster: Util::Statement.ensure(cluster, "ON CLUSTER #{cluster}") } execute(format(sql, pattern)).success? end end end end ================================================ FILE: lib/click_house/extend/type_definition.rb ================================================ # frozen_string_literal: true module ClickHouse module Extend module TypeDefinition def types @types ||= Hash.new(Type::UndefinedType.new) end def add_type(type, klass) types[type] = klass end end end end ================================================ FILE: lib/click_house/extend.rb ================================================ # frozen_string_literal: true module ClickHouse module Extend autoload :TypeDefinition, 'click_house/extend/type_definition' autoload :Configurable, 'click_house/extend/configurable' autoload :Connectible, 'click_house/extend/connectible' autoload :ConnectionHealthy, 'click_house/extend/connection_healthy' autoload :ConnectionDatabase, 'click_house/extend/connection_database' autoload :ConnectionTable, 'click_house/extend/connection_table' autoload :ConnectionSelective, 'click_house/extend/connection_selective' autoload :ConnectionInserting, 'click_house/extend/connection_inserting' autoload :ConnectionAltering, 'click_house/extend/connection_altering' autoload :ConnectionExplaining, 'click_house/extend/connection_explaining' end end ================================================ FILE: lib/click_house/middleware/logging.rb ================================================ # frozen_string_literal: true module ClickHouse module Middleware class Logging < Faraday::Middleware Faraday::Response.register_middleware self => self EMPTY = '' GET = :get attr_reader :logger, :starting def initialize(app = nil, logger:) @logger = logger super(app) end def call(env) @starting = timestamp super end # rubocop:disable Layout/LineLength def on_complete(env) summary = SummaryMiddleware.extract(env) logger.info("\e[1mSQL (#{duration_stats_log(summary)})\e[0m #{query(env)};") logger.debug(env.request_body) if log_body?(env) logger.info("\e[1mRead: #{summary.read_rows} rows, #{summary.read_bytes_pretty}. Written: #{summary.written_rows} rows, #{summary.written_bytes_pretty}\e[0m") end # rubocop:enable Layout/LineLength private def duration timestamp - starting end def timestamp Process.clock_gettime(Process::CLOCK_MONOTONIC) end # @return [Boolean] def log_body?(env) return unless logger.debug? return if env.method == GET # GET queries logs body as a statement return if env.request_body.nil? || env.request_body == EMPTY true end def query(env) if env.method == GET env.request_body else String(CGI.parse(env.url.query.to_s).dig('query', 0) || '[NO QUERY]').chomp end end def duration_stats_log(summary) "Total: #{Util::Pretty.measure(duration * 1000)}, CH: #{summary.elapsed_pretty}" end end end end ================================================ FILE: lib/click_house/middleware/parse_csv.rb ================================================ # frozen_string_literal: true module ClickHouse module Middleware class ParseCsv < ResponseBase Faraday::Response.register_middleware self => self # @param env [Faraday::Env] def on_complete(env) return unless content_type?(env, content_type) env.body = env.body.strip.empty? ? nil : CSV.parse(env.body) end end end end ================================================ FILE: lib/click_house/middleware/parse_json.rb ================================================ # frozen_string_literal: true module ClickHouse module Middleware class ParseJson < ResponseBase Faraday::Response.register_middleware self => self # @param env [Faraday::Env] def on_complete(env) return unless content_type?(env, content_type) env.body = JSON.parse(env.body, config.json_load_options) unless env.body.strip.empty? end end end end ================================================ FILE: lib/click_house/middleware/parse_json_oj.rb ================================================ # frozen_string_literal: true module ClickHouse module Middleware class ParseJsonOj < ResponseBase Faraday::Response.register_middleware self => self # @param env [Faraday::Env] def on_complete(env) return unless content_type?(env, content_type) env.body = Oj.load(env.body, config.oj_load_options) unless env.body.strip.empty? end private def on_setup require 'oj' unless defined?(Oj) end end end end ================================================ FILE: lib/click_house/middleware/raise_error.rb ================================================ # frozen_string_literal: true module ClickHouse module Middleware class RaiseError < Faraday::Middleware EXCEPTION_CODE_HEADER = 'x-clickhouse-exception-code' Faraday::Response.register_middleware self => self # @param env [Faraday::Env] def call(env) super rescue Faraday::ConnectionFailed => e raise NetworkException, e.message, e.backtrace end # @param env [Faraday::Env] def on_complete(env) if env.response_headers.include?(EXCEPTION_CODE_HEADER) || !env.success? raise DbException, "[#{env.status}] #{env.body}" end end end end end ================================================ FILE: lib/click_house/middleware/response_base.rb ================================================ # frozen_string_literal: true module ClickHouse module Middleware class ResponseBase < Faraday::Middleware CONTENT_TYPE_HEADER = 'content-type' attr_reader :options attr_reader :content_type def initialize(app = nil, options: {}, content_type: nil, preserve_raw: false) super(app) @options = options @content_type = content_type @preserve_raw = preserve_raw on_setup end # @return [Boolean] # @param env [Faraday::Env] # @param regexp [NilClass, Regexp] def content_type?(env, regexp) case regexp when NilClass false when Regexp regexp.match?(String(env[:response_headers][CONTENT_TYPE_HEADER])) else raise ArgumentError, "expected regexp got #{regexp.class}" end end # @return [Config] def config options.fetch(:config) end private def on_setup # require external dependencies here end end end end ================================================ FILE: lib/click_house/middleware/summary_middleware.rb ================================================ # frozen_string_literal: true module ClickHouse module Middleware class SummaryMiddleware < ResponseBase Faraday::Response.register_middleware self => self KEY = :summary # @param env [Faraday::Env] # @return [Response::Summary] def self.extract(env) env.custom_members.fetch(KEY) end # @param env [Faraday::Env] def on_complete(env) env.custom_members[KEY] = Response::Summary.new( config, headers: env.response_headers, body: env.body.is_a?(Hash) ? env.body : {} ) end end end end ================================================ FILE: lib/click_house/middleware.rb ================================================ # frozen_string_literal: true module ClickHouse module Middleware autoload :ResponseBase, 'click_house/middleware/response_base' autoload :SummaryMiddleware, 'click_house/middleware/summary_middleware' autoload :Logging, 'click_house/middleware/logging' autoload :ParseCsv, 'click_house/middleware/parse_csv' autoload :ParseJsonOj, 'click_house/middleware/parse_json_oj' autoload :ParseJson, 'click_house/middleware/parse_json' autoload :RaiseError, 'click_house/middleware/raise_error' end end ================================================ FILE: lib/click_house/response/factory.rb ================================================ # frozen_string_literal: true module ClickHouse module Response class Factory KEY_META = 'meta' KEY_DATA = 'data' # @return [ResultSet] # @params faraday [Faraday::Response] # @params config [Config] def self.response(faraday, config) body = faraday.body # wrap to be able to use connection#select_one, connection#select_value # with other formats like binary return raw(faraday, config) unless body.is_a?(Hash) return raw(faraday, config) unless body.key?(config.key(KEY_META)) && body.key?(config.key(KEY_DATA)) ResultSet.new( config: config, meta: body.fetch(config.key(KEY_META)), data: body.fetch(config.key(KEY_DATA)), summary: Middleware::SummaryMiddleware.extract(faraday.env) ) end # @return [ResultSet] # Rae ResultSet (without type casting) def self.raw(faraday, config) ResultSet.raw( config: config, data: Util.array(faraday.body), summary: Middleware::SummaryMiddleware.extract(faraday.env) ) end # Result of execution # @return [Response::Summary] # @params faraday [Faraday::Response] def self.exec(faraday) Middleware::SummaryMiddleware.extract(faraday.env) end # @return [Response::Summary] def self.empty_exec(config) Summary.new(config) end end end end ================================================ FILE: lib/click_house/response/result_set.rb ================================================ # frozen_string_literal: true module ClickHouse module Response class ResultSet extend Forwardable include Enumerable KEY_META_NAME = 'name' KEY_META_TYPE = 'type' def_delegators :to_a, :inspect, :each, :fetch, :length, :count, :size, :first, :last, :[], :to_h def_delegators :summary, :statistics, :headers, :totals, :rows_before_limit_at_least attr_reader :config, :meta, :data, :summary class << self # @param config [Config] # @return [ResultSet] def raw(config:, data:, summary:) new(config: config, data: data, to_a: data, meta: [], summary: summary) end end # @param config [Config] # @param meta [Array] # @param data [Array] # @param summary [Response::Summary] def initialize(config:, meta:, data:, summary:, to_a: nil) @config = config @meta = meta @data = data @summary = summary @to_a = to_a end # @return [Array, Hash] # @param data [Array, Hash] def serialize(data) case data when Hash serialize_one(data) when Array data.map(&method(:serialize_one)) else raise ArgumentError, "expect Hash or Array, got: #{data.class}" end end # @return [Hash] # @param row [Hash] def serialize_one(row) row.each_with_object({}) do |(key, value), object| object[key] = serialize_column(key, value) end end # @param name [String] column name # @param value [Any] def serialize_column(name, value) stmt = types.fetch(name) serialize_type(stmt, value) rescue KeyError => e raise SerializeError, "field <#{name}> does not exists in table schema: #{types}", e.backtrace rescue StandardError => e raise SerializeError, "failed to serialize <#{name}> with #{stmt}, #{e.class}, #{e.message}", e.backtrace end def to_a @to_a ||= data.each do |row| row.each do |name, value| row[name] = cast_type(types.fetch(name), value) end end end # @return [Hash] def types @types ||= meta.each_with_object({}) do |row, object| column = row.fetch(config.key(KEY_META_NAME)) # make symbol keys, if config.symbolize_keys is true, # to be able to cast and serialize properly object[config.key(column)] = begin current = Ast::Parser.new(row.fetch(config.key(KEY_META_TYPE))).parse assign_type(current) current end end end private # @param stmt [Ast::Statement] def assign_type(stmt) stmt.caster = ClickHouse.types[stmt.name] if stmt.caster.is_a?(Type::UndefinedType) placeholders = stmt.arguments.map(&:placeholder) stmt.caster = ClickHouse.types["#{stmt.name}(#{placeholders.join(', ')})"] end stmt.arguments.each(&method(:assign_type)) end # @param stmt [Ast::Statement] def cast_type(stmt, value) return cast_container(stmt, value) if stmt.caster.container? return cast_map(stmt, Hash(value)) if stmt.caster.map? return cast_tuple(stmt, Array(value)) if stmt.caster.tuple? stmt.caster.cast(value, *stmt.argument_values) end # @return [Hash] # @param stmt [Ast::Statement] # @param hash [Hash] def cast_map(stmt, hash) raise ArgumentError, "expect hash got #{hash.class}" unless hash.is_a?(Hash) key_type, value_type = stmt.arguments hash.each_with_object({}) do |(key, value), object| object[cast_type(key_type, key)] = cast_type(value_type, value) end end # @param stmt [Ast::Statement] def cast_container(stmt, value) stmt.caster.cast_each(value) do |item| cast_type(stmt.argument_first!, item) end end # @param stmt [Ast::Statement] def cast_tuple(stmt, value) value.map.with_index do |item, ix| cast_type(stmt.arguments.fetch(ix), item) end end # @param stmt [Ast::Statement] def serialize_type(stmt, value) return serialize_container(stmt, value) if stmt.caster.container? return serialize_map(stmt, value) if stmt.caster.map? return serialize_tuple(stmt, Array(value)) if stmt.caster.tuple? stmt.caster.serialize(value, *stmt.argument_values) end # @param stmt [Ast::Statement] def serialize_container(stmt, value) stmt.caster.serialize_each(value) do |item| serialize_type(stmt.argument_first!, item) end end # @return [Hash] # @param stmt [Ast::Statement] # @param hash [Hash] def serialize_map(stmt, hash) raise ArgumentError, "expect hash got #{hash.class}" unless hash.is_a?(Hash) key_type, value_type = stmt.arguments hash.each_with_object({}) do |(key, value), object| object[serialize_type(key_type, key)] = serialize_type(value_type, value) end end # @param stmt [Ast::Statement] def serialize_tuple(stmt, value) value.map.with_index do |item, ix| serialize_type(stmt.arguments.fetch(ix), item) end end end end end ================================================ FILE: lib/click_house/response/summary.rb ================================================ # frozen_string_literal: true module ClickHouse module Response class Summary SUMMARY_HEADER = 'x-clickhouse-summary' KEY_TOTALS = 'totals' KEY_STATISTICS = 'statistics' KEY_ROWS_BEFORE_LIMIT_AT_LEAST = 'rows_before_limit_at_least' KEY_STAT_ELAPSED = 'elapsed' attr_reader :config, :headers, :summary, # {:elapsed=>0.387287e-3, :rows_read=>0, :bytes_read=>0}} :statistics, :totals, :rows_before_limit_at_least # @param config [Config] # @param headers [Faraday::Utils::Headers] # @param body [Hash] # TOTALS [Array|Hash|NilClass] Support for 'GROUP BY WITH TOTALS' modifier # https://clickhouse.tech/docs/en/sql-reference/statements/select/group-by/#with-totals-modifier # Hash in JSON format and Array in JSONCompact def initialize(config, headers: Faraday::Utils::Headers.new, body: {}) @headers = headers @config = config @statistics = body.fetch(config.key(KEY_STATISTICS), {}) @totals = body[config.key(KEY_TOTALS)] @rows_before_limit_at_least = body[config.key(KEY_ROWS_BEFORE_LIMIT_AT_LEAST)] @summary = parse_summary(headers[SUMMARY_HEADER]) end # @return [Integer] def read_rows summary[config.key('read_rows')].to_i end # @return [Integer] def read_bytes summary[config.key('read_bytes')].to_i end # @return [String] def read_bytes_pretty Util::Pretty.size(read_bytes) end # @return [Integer] def written_rows summary[config.key('written_rows')].to_i end # @return [Integer] def written_bytes summary[config.key('written_bytes')].to_i end # @return [String] def written_bytes_pretty Util::Pretty.size(written_bytes) end # @return [Integer] def total_rows_to_read summary[config.key('total_rows_to_read')].to_i end # @return [Integer] def result_rows summary[config.key('result_rows')].to_i end # @return [Integer] def result_bytes summary[config.key('result_bytes')].to_i end # @return [Float] def elapsed statistics[config.key(KEY_STAT_ELAPSED)].to_f end # @return [String] def elapsed_pretty Util::Pretty.measure(elapsed * 1000) end private # @return [Hash] # { # "read_rows" => "1", # "read_bytes" => "23", # "written_rows" => "1", # "written_bytes" => "23", # "total_rows_to_read" => "0", # "result_rows" => "1", # "result_bytes" => "23", # } def parse_summary(value) return {} if value.nil? || value.empty? JSON.parse(value) end end end end ================================================ FILE: lib/click_house/response.rb ================================================ # frozen_string_literal: true module ClickHouse module Response autoload :Factory, 'click_house/response/factory' autoload :ResultSet, 'click_house/response/result_set' autoload :Summary, 'click_house/response/summary' end end ================================================ FILE: lib/click_house/serializer/base.rb ================================================ # frozen_string_literal: true module ClickHouse module Serializer class Base attr_reader :config # @param config [Config] def initialize(config) @config = config on_setup end def dump(data) raise NotImplementedError, __method__ end # @return [String] # @param data [Array] def dump_each_row(data, sep = "\n") data.map(&method(:dump)).join(sep) end private # require external dependencies here def on_setup nil end end end end ================================================ FILE: lib/click_house/serializer/json_oj_serializer.rb ================================================ # frozen_string_literal: true module ClickHouse module Serializer class JsonOjSerializer < Base def dump(data) Oj.dump(data, config.oj_dump_options) end private def on_setup require 'oj' unless defined?(Oj) end end end end ================================================ FILE: lib/click_house/serializer/json_serializer.rb ================================================ # frozen_string_literal: true module ClickHouse module Serializer class JsonSerializer < Base def dump(data) JSON.dump(data) end end end end ================================================ FILE: lib/click_house/serializer.rb ================================================ # frozen_string_literal: true module ClickHouse module Serializer autoload :Base, 'click_house/serializer/base' autoload :JsonSerializer, 'click_house/serializer/json_serializer' autoload :JsonOjSerializer, 'click_house/serializer/json_oj_serializer' end end ================================================ FILE: lib/click_house/type/array_type.rb ================================================ # frozen_string_literal: true module ClickHouse module Type class ArrayType < BaseType def cast_each(value, *_argv, &block) value.map(&block) end def serialize_each(value, *_argv, &block) value.map(&block) end def container? true end def ddl? false end end end end ================================================ FILE: lib/click_house/type/base_type.rb ================================================ # frozen_string_literal: true module ClickHouse module Type class BaseType def cast(_value, *) raise NotImplementedError, __method__ end def cast_each(_value, *) raise NotImplementedError, __method__ end def serialize_each(_value, *) raise NotImplementedError, __method__ end # @return [Boolean] # true if type contains another type like Nullable(T) or Array(T) def container? false end # @return [Boolean] # true if type is a Map def map? false end # @return [Boolean] # true if type is a Tuple def tuple? false end # @return [Boolean] # skip type from DDL statements def ddl? true end end end end ================================================ FILE: lib/click_house/type/boolean_type.rb ================================================ # frozen_string_literal: true module ClickHouse module Type class BooleanType < BaseType TRUE_VALUE = 1 FALSE_VALUE = 0 def cast(value) case value when TrueClass, FalseClass value else value.to_i == TRUE_VALUE end end def serialize(value) value ? TRUE_VALUE : FALSE_VALUE end end end end ================================================ FILE: lib/click_house/type/date_time64_type.rb ================================================ # frozen_string_literal: true module ClickHouse module Type class DateTime64Type < BaseType BASE_FORMAT = '%Y-%m-%d %H:%M:%S' CAST_FORMAT = "#{BASE_FORMAT}.%N" SERIALIZE_FORMATS = { 0 => BASE_FORMAT, 1 => "#{BASE_FORMAT}.%1N", 2 => "#{BASE_FORMAT}.%2N", 3 => "#{BASE_FORMAT}.%3N", 4 => "#{BASE_FORMAT}.%4N", 5 => "#{BASE_FORMAT}.%5N", 6 => "#{BASE_FORMAT}.%6N", 7 => "#{BASE_FORMAT}.%7N", 8 => "#{BASE_FORMAT}.%8N", 9 => "#{BASE_FORMAT}.%9N", }.freeze # Tick size (precision): # 10-precision seconds. # Valid range: [ 0 : 9 ]. # Typically are used - 3 (milliseconds), 6 (microseconds), 9 (nanoseconds). def cast(value, precision = 0, tz = nil) format = precision.zero? ? BASE_FORMAT : CAST_FORMAT if tz Time.find_zone(tz).strptime(value, format) else Time.strptime(value, format) end end def serialize(value, precision = 3, _tz = nil) value.strftime(SERIALIZE_FORMATS.fetch(precision)) end end end end ================================================ FILE: lib/click_house/type/date_time_type.rb ================================================ # frozen_string_literal: true module ClickHouse module Type class DateTimeType < BaseType FORMAT = '%Y-%m-%d %H:%M:%S' def cast(value, tz = nil) if tz Time.find_zone(tz).strptime(value, FORMAT) else Time.strptime(value, FORMAT) end end def serialize(value, *) value.strftime(FORMAT) end end end end ================================================ FILE: lib/click_house/type/date_type.rb ================================================ # frozen_string_literal: true module ClickHouse module Type class DateType < BaseType FORMAT = '%Y-%m-%d' def cast(value) Date.strptime(value, FORMAT) end def serialize(value) value.strftime(FORMAT) end end end end ================================================ FILE: lib/click_house/type/decimal_type.rb ================================================ # frozen_string_literal: true module ClickHouse module Type class DecimalType < BaseType MAXIMUM = Float::DIG.next # clickhouse: # P - precision. Valid range: [ 1 : 76 ]. Determines how many decimal digits number can have (including fraction). # S - scale. Valid range: [ 0 : P ]. Determines how many decimal digits fraction can have. # # when Oj parser @refs https://stackoverflow.com/questions/47885304/deserialise-json-numbers-as-bigdecimal def cast(value, precision = MAXIMUM, _scale = nil) case value when BigDecimal value when String BigDecimal(value) else BigDecimal(value, precision > MAXIMUM ? MAXIMUM : precision) end end # @return [BigDecimal] def serialize(value, precision = MAXIMUM, _scale = nil) cast(value, precision) end end end end ================================================ FILE: lib/click_house/type/fixed_string_type.rb ================================================ # frozen_string_literal: true module ClickHouse module Type class FixedStringType < BaseType def cast(value, _limit = nil) value.to_s end def serialize(value, limit = nil) value[0..(limit ? limit.pred : -1)] unless value.nil? end end end end ================================================ FILE: lib/click_house/type/float_type.rb ================================================ # frozen_string_literal: true module ClickHouse module Type class FloatType < BaseType def cast(value) Float(value) unless value.nil? end def serialize(value) value.to_f unless value.nil? end end end end ================================================ FILE: lib/click_house/type/integer_type.rb ================================================ # frozen_string_literal: true module ClickHouse module Type class IntegerType < BaseType def cast(value) Integer(value) end def serialize(value) value.to_i end end end end ================================================ FILE: lib/click_house/type/ip_type.rb ================================================ # frozen_string_literal: true module ClickHouse module Type class IPType < BaseType def cast(value) IPAddr.new(value) end def serialize(value) value.to_s end end end end ================================================ FILE: lib/click_house/type/low_cardinality_type.rb ================================================ # frozen_string_literal: true module ClickHouse module Type class LowCardinalityType < BaseType def cast_each(value, *_argv) yield(value) end def serialize_each(value, *_argv) yield(value) end def container? true end end end end ================================================ FILE: lib/click_house/type/map_type.rb ================================================ # frozen_string_literal: true module ClickHouse module Type class MapType < BaseType def map? true end def ddl? false end end end end ================================================ FILE: lib/click_house/type/nullable_type.rb ================================================ # frozen_string_literal: true module ClickHouse module Type class NullableType < BaseType def cast_each(value, *_argv) yield(value) unless value.nil? end def serialize_each(value, *_argv) yield(value) unless value.nil? end def container? true end def ddl? false end end end end ================================================ FILE: lib/click_house/type/string_type.rb ================================================ # frozen_string_literal: true module ClickHouse module Type class StringType < BaseType def cast(value, *) value.to_s end def serialize(value, *) value.to_s end end end end ================================================ FILE: lib/click_house/type/tuple_type.rb ================================================ # frozen_string_literal: true module ClickHouse module Type class TupleType < BaseType def tuple? true end def ddl? false end end end end ================================================ FILE: lib/click_house/type/undefined_type.rb ================================================ # frozen_string_literal: true module ClickHouse module Type class UndefinedType < BaseType def cast(value, *) value end def serialize(value, *) value end end end end ================================================ FILE: lib/click_house/type.rb ================================================ # frozen_string_literal: true module ClickHouse module Type autoload :BaseType, 'click_house/type/base_type' autoload :NullableType, 'click_house/type/nullable_type' autoload :UndefinedType, 'click_house/type/undefined_type' autoload :DateType, 'click_house/type/date_type' autoload :DateTimeType, 'click_house/type/date_time_type' autoload :DateTime64Type, 'click_house/type/date_time64_type' autoload :IntegerType, 'click_house/type/integer_type' autoload :FloatType, 'click_house/type/float_type' autoload :BooleanType, 'click_house/type/boolean_type' autoload :DecimalType, 'click_house/type/decimal_type' autoload :FixedStringType, 'click_house/type/fixed_string_type' autoload :ArrayType, 'click_house/type/array_type' autoload :TupleType, 'click_house/type/tuple_type' autoload :MapType, 'click_house/type/map_type' autoload :StringType, 'click_house/type/string_type' autoload :IPType, 'click_house/type/ip_type' autoload :LowCardinalityType, 'click_house/type/low_cardinality_type' end end ================================================ FILE: lib/click_house/util/pretty.rb ================================================ # frozen_string_literal: true module ClickHouse module Util module Pretty SIZE_UNITS = %w[B KiB MiB GiB TiB Pib EiB].freeze module_function # rubocop:disable all def size(bytes) return '0B' if bytes == 0 exp = (Math.log(bytes) / Math.log(1024)).to_i exp = 6 if exp > 6 format('%.1f%s', bytes.to_f / 1024**exp, SIZE_UNITS[exp]) end # rubocop:enable all def measure(ms) "#{ms.round}MS" end def squish(string) string.gsub(/[[:space:]]+/, ' ').strip end end end end ================================================ FILE: lib/click_house/util/statement.rb ================================================ # frozen_string_literal: true module ClickHouse module Util module Statement module_function def ensure(truthful, value, fallback = nil) truthful ? value : fallback end end end end ================================================ FILE: lib/click_house/util.rb ================================================ # frozen_string_literal: true module ClickHouse module Util autoload :Statement, 'click_house/util/statement' autoload :Pretty, 'click_house/util/pretty' module_function # wraps def array(input) input.is_a?(Array) ? input : [input] end end end ================================================ FILE: lib/click_house/version.rb ================================================ # frozen_string_literal: true module ClickHouse VERSION = '2.1.2' end ================================================ FILE: lib/click_house.rb ================================================ # frozen_string_literal: true require 'date' require 'json' require 'csv' require 'uri' require 'logger' require 'faraday' require 'forwardable' require 'bigdecimal' require 'active_support/core_ext/time/calculations' require 'click_house/version' require 'click_house/errors' require 'click_house/response' require 'click_house/serializer' require 'click_house/type' require 'click_house/middleware' require 'click_house/extend' require 'click_house/ast' require 'click_house/util' require 'click_house/definition' module ClickHouse extend Extend::TypeDefinition extend Extend::Configurable extend Extend::Connectible autoload :Config, 'click_house/config' autoload :Connection, 'click_house/connection' add_type 'Array', Type::ArrayType.new add_type 'Nullable', Type::NullableType.new add_type 'Map', Type::MapType.new add_type 'LowCardinality', Type::LowCardinalityType.new add_type 'Tuple', Type::TupleType.new %w[Bool].each do |column| add_type column, Type::BooleanType.new end %w[Date].each do |column| add_type column, Type::DateType.new end %w[String FixedString(%d) UUID].each do |column| add_type column, Type::StringType.new end %w[DateTime DateTime(%s)].each do |column| add_type column, Type::DateTimeType.new end ['DateTime64(%d)', 'DateTime64(%d, %s)'].each do |column| add_type column, Type::DateTime64Type.new end ['Decimal(%d, %d)', 'Decimal32(%d)', 'Decimal64(%d)', 'Decimal128(%d)', 'Decimal256(%d)'].each do |column| add_type column, Type::DecimalType.new end %w[UInt8 UInt16 UInt32 UInt64 Int8 Int16 Int32 Int64].each do |column| add_type column, Type::IntegerType.new end %w[Float32 Float64].each do |column| add_type column, Type::FloatType.new end %w[IPv4 IPv6].each do |column| add_type column, Type::IPType.new end end ================================================ FILE: log/.keep ================================================ ================================================ FILE: spec/click_house/ast/parser_spec.rb ================================================ RSpec.describe ClickHouse::Ast::Parser do let(:expectations) do { "Int" => 'Int', "DateTime('Asia/Istanbul')" => "DateTime('Asia/Istanbul')", "Array(Int, String(2))" => "Array(Int,String(2))", "Array(Array(Array(Array(Nullable(Int, String)))))" => "Array(Array(Array(Array(Nullable(Int,String)))))", "Function(Decimal(1, 2), Map(Decimal(3, 4), Decimal(5, 6)))" => "Function(Decimal(1,2),Map(Decimal(3,4),Decimal(5,6)))", "Array(Map(Decimal(1, 2), Decimal(3, 4)))" => "Array(Map(Decimal(1,2),Decimal(3,4)))", "Map(Decimal(1, 2), Decimal(3, 4))" => "Map(Decimal(1,2),Decimal(3,4))", "Map(Decimal(1,2))" => "Map(Decimal(1,2))", "Map(String, Decimal(1,2))" => "Map(String,Decimal(1,2))", "Decimal(1,2)" => "Decimal(1,2)", "A(1)" => "A(1)", "A(1, 2)" => "A(1,2)", "A(B(1))" => "A(B(1))", "A(B(1), B(2))" => "A(B(1),B(2))", "Enum8('hello' = 1, 'world' = 2)" => "Enum8('hello' = 1,'world' = 2)" } end it 'works' do expectations.each do |statement, expect| expect(described_class.new(statement).parse.to_s).to eq(expect) end end context 'when Array with nested type' do subject do described_class.new("Array(String(2))").parse end it 'works' do expect(subject.name).to eq('Array') expect(subject.arguments).to have_attributes(size: 1) expect(subject.arguments.first.name).to eq("String") expect(subject.arguments.first.arguments).to have_attributes(size: 1) expect(subject.arguments.first.arguments.first.name).to eq("2") end end context 'when space between arguments' do subject do described_class.new("Foo(10, 'bar')").parse end it 'works' do expect(subject.name).to eq('Foo') expect(subject.arguments.map(&:placeholder)).to eq(%w[%d %s]) expect(subject.arguments.map(&:value)).to eq([10, 'bar']) end end end ================================================ FILE: spec/click_house/config_spec.rb ================================================ RSpec.describe ClickHouse::Config do describe '#assign' do it 'works' do expect { subject.assign(port: 33) }.to change { subject.port }.to(33) end it 'returns self' do expect(subject.assign({})).to be_a(described_class) end end describe '#initialize' do context 'when params' do it 'works' do expect(described_class.new(port: 33).port).to eq(33) end end context 'when block' do it 'works' do expect(described_class.new { |c| c.port = 33 }.port).to eq(33) end end end describe '#auth?' do context 'when credentials empty' do before do subject.username = nil subject.password = nil end it 'is false' do expect(subject.auth?).to eq(false) end end context 'when credentials exists' do before do subject.username = 'foo' subject.password = 'bar' end it 'is true' do expect(subject.auth?).to eq(true) end end end describe '#url!' do before do subject.url = 'http://example.com' subject.scheme = 'https' subject.host = 'clickhouse' subject.port = '3344' end context 'when url exists' do it 'works' do expect(subject.url!).to eq('http://example.com') end end context 'when url empty' do before do subject.url = nil end it 'works' do expect(subject.url!).to eq('https://clickhouse:3344') end end end end ================================================ FILE: spec/click_house/connection_spec.rb ================================================ RSpec.describe ClickHouse::Connection do context 'when basic auth' do subject do ClickHouse::Connection.new(ClickHouse.config.clone.assign( username: 'user', password: 'password' )) end it 'works' do expect { subject.tables }.to raise_error(ClickHouse::DbException, /Authentication failed/) end end end ================================================ FILE: spec/click_house/definition/column_set_spec.rb ================================================ RSpec.describe ClickHouse::Definition::ColumnSet do def squish(string) string.gsub(/[[:space:]]/, '').strip end context 'when integration' do subject do described_class.new do |t| t.Decimal :money, 5, 5 t.UInt16 :year_birth, low_cardinality: true t.UInt16 :year_death, low_cardinality: true, nullable: true, default: 0 t.Float32 :city_id, default: 0, nullable: true t.Nested :json do |n| n.UInt8 :cid, nullable: true n.Date :created_at, default: 'NOW()' n.DateTime :updated_at, 'UTC' n.DateTime64 :deleted_at, 6, 'UTC' end t << "words Enum('hello' = 1, 'world' = 2)" t << "tags Array(String)" end end let(:expectation) do <<~SQL ( money Decimal(5, 5), year_birth LowCardinality(UInt16), year_death LowCardinality(Nullable(UInt16)) DEFAULT 0, city_id Nullable(Float32) DEFAULT 0, json Nested ( cid Nullable(UInt8) , created_at Date DEFAULT NOW(), updated_at DateTime('UTC'), deleted_at DateTime64(6, 'UTC') ), words Enum('hello' = 1, 'world' = 2), tags Array(String) ) SQL end it 'works' do expect(squish(subject.to_s)).to eq(squish(expectation)) end end end ================================================ FILE: spec/click_house/extend/connection_altering_spec.rb ================================================ RSpec.describe ClickHouse::Extend::ConnectionAltering do subject do ClickHouse.connection end describe '#add_column' do before do subject.execute <<~SQL CREATE TABLE rspec (date Date, id UInt32, user_id UInt32) ENGINE MergeTree() ORDER BY date SQL end context 'when not exists' do before do subject.add_column(:rspec, :account_id, :UInt64, default: 0, after: :date) end let(:column) do subject.describe_table('rspec').find { |r| r['name'] == 'account_id' } end it 'works' do expect(subject.describe_table('rspec').map { |r| r['name'] }).to eq(%w[date account_id id user_id]) expect(column).to include('type' => 'UInt64') expect(column).to include('default_expression' => '0') end end context 'when exists' do let(:function) do subject.add_column(:rspec, :user_id, 'UInt32') end it 'errors' do expect { function }.to raise_error(ClickHouse::DbException) end end context 'when if not exists' do let(:function) do subject.add_column(:rspec, :user_id, 'UInt32', if_not_exists: true) end it 'works' do expect(function).to eq(true) end end end describe '#drop_column' do before do subject.execute <<~SQL CREATE TABLE rspec (date Date, id UInt32, int_1 UInt32) ENGINE MergeTree() ORDER BY date SQL end context 'when exists' do let(:function) do subject.drop_column('rspec', :int_1) end it 'works' do expect { function }.to change { subject.describe_table('rspec').length }.by(-1) end end context 'when not exists' do let(:function) do subject.drop_column('rspec', :foo) end it 'errors' do expect { function }.to raise_error(ClickHouse::DbException) end end context 'when if exists' do let(:function) do subject.drop_column('rspec', :foo, if_exists: true) end it 'works' do expect(function).to eq(true) end end end describe '#modify_column' do before do subject.execute <<~SQL CREATE TABLE rspec (date Date, id UInt32, int_1 UInt32) ENGINE MergeTree() ORDER BY date SQL end context 'when exists' do let(:function) do subject.modify_column('rspec', 'int_1', type: :UInt64, default: 0) end let(:column) do -> { subject.describe_table('rspec').find { |r| r['name'] == 'int_1' } } end it 'works' do expect { function }.to change { column.call.values_at('type', 'default_expression') }.to(['UInt64', '0']) end end context 'when not exists' do let(:function) do subject.modify_column('rspec', 'foo', type: :UInt64) end it 'errors' do expect { function }.to raise_error(ClickHouse::DbException) end end context 'when if exists' do let(:function) do subject.modify_column('rspec', 'foo', type: :UInt64, if_exists: true) end it 'works' do expect(function).to eq(true) end end end describe '#alter_table' do before do subject.execute <<~SQL CREATE TABLE rspec (date Date, id UInt32, int_1 UInt32) ENGINE MergeTree() ORDER BY date SQL end context 'when argument' do let(:function) do subject.alter_table('rspec', 'DROP COLUMN int_1') end it 'works' do expect { function }.to change { subject.describe_table('rspec').length }.by(-1) end end context 'when block' do let(:function) do subject.alter_table('rspec') do 'DROP COLUMN int_1' end end it 'works' do expect { function }.to change { subject.describe_table('rspec').length }.by(-1) end end end describe '#add_index, #drop_index' do before do subject.execute <<~SQL CREATE TABLE rspec(a String, b Array(String)) engine=MergeTree() order by a; SQL end it 'works' do expect(subject.add_index('rspec', 'ix', 'has(b, a)', type: 'minmax', granularity: 2)).to eq(true) expect(subject.drop_index('rspec', 'ix')).to eq(true) end end end ================================================ FILE: spec/click_house/extend/connection_database_spec.rb ================================================ RSpec.describe ClickHouse::Extend::ConnectionDatabase do subject do ClickHouse.connection end describe '#databases' do it 'works' do expect(subject.databases).to include('default', 'system', ClickHouse.config.database) end end describe '#create_database' do context 'when exists' do it 'errors' do expect { subject.create_database(ClickHouse.config.database) }.to raise_error(ClickHouse::DbException) end end context 'when if not exists' do it 'works' do expect(subject.create_database(ClickHouse.config.database, if_not_exists: true)).to eq(true) end end context 'when default' do it 'works' do expect(subject.create_database('foo')).to eq(true) end end context 'when engine' do it 'works' do expect(subject.create_database('foo', engine: 'Lazy(10)')).to eq(true) end end end describe '#drop_database' do context 'when not exists' do it 'errors' do expect { subject.drop_database('foo') }.to raise_error(ClickHouse::DbException) end end context 'when exists' do it 'works' do expect(subject.drop_database('foo', if_exists: true)).to eq(true) end end end end ================================================ FILE: spec/click_house/extend/connection_explaining_spec.rb ================================================ RSpec.describe ClickHouse::Extend::ConnectionExplaining do subject do ClickHouse.connection end before do subject.execute <<~SQL CREATE TABLE rspec(id Int64) ENGINE TinyLog SQL end let(:expectation) do <<~TXT Expression ((Projection + Before ORDER BY)) Join (JOIN FillRightFirst) Expression (Before JOIN) ReadFromStorage (TinyLog) Expression ((Joined actions + (Rename joined columns + (Projection + Before ORDER BY)))) ReadFromStorage (TinyLog) TXT end context 'when normal query' do it 'works' do buffer = StringIO.new subject.explain('SELECT 1 FROM rspec CROSS JOIN rspec', io: buffer) expect(buffer.string).to eq(expectation) end end context 'when EXPLAIN query' do it 'works' do buffer = StringIO.new subject.explain('EXPLAIN SELECT 1 FROM rspec CROSS JOIN rspec', io: buffer) expect(buffer.string).to eq(expectation) end end end ================================================ FILE: spec/click_house/extend/connection_healthy_spec.rb ================================================ RSpec.describe ClickHouse::Extend::ConnectionHealthy do subject do ClickHouse::Connection.new(ClickHouse.config) end describe '#ping' do context 'when ok' do it 'works' do expect(subject.ping).to eq(true) end end context 'when fail' do before do subject.transport.port = '80' end it 'errors' do expect { subject.ping }.to raise_error(ClickHouse::NetworkException) end end end describe '#replicas_status' do context 'when ok' do it 'works' do expect(subject.replicas_status).to eq(true) end end context 'when fail' do before do subject.transport.port = '80' end it 'errors' do expect { subject.replicas_status }.to raise_error(ClickHouse::NetworkException) end end end end ================================================ FILE: spec/click_house/extend/connection_inserting_spec.rb ================================================ # ⚠️ INSERT IN TESTS SHOULD HAVE A DIFFERENT ORDER OF COLUMNS # FROM THE ORDER IN THE TABLE ITSELF RSpec.describe ClickHouse::Extend::ConnectionInserting do subject do ClickHouse.connection end before do subject.execute <<~SQL CREATE TABLE rspec(id Int64, name Nullable(String)) ENGINE Memory SQL end def expected(insert, count) expect(insert.written_rows).to eq(count) expect(subject.select_value('SELECT COUNT(*) FROM rspec')).to eq(count) end context 'when blank' do let(:insert) do subject.insert('rspec') end it 'works' do expected(insert, 0) end end context 'when columns with blank values' do let(:insert) do subject.insert('rspec', columns: %i[id name]) end it 'works' do expected(insert, 0) end end describe 'execution' do let(:insert) do subject.insert('rspec', values: {id: 1, name: 'foo'}) end it 'has proper attributes' do expect(insert.read_rows).to be > 0 expect(insert.read_bytes).to be > 0 expect(insert.written_rows).to be > 0 expect(insert.written_bytes).to be > 0 expect(insert.result_rows).to be > 0 expect(insert.result_bytes).to be > 0 expect(insert.summary).not_to be_empty expect(insert.headers).not_to be_empty end end context 'when body', if: ruby_version_gt('3') do context 'when Hash' do let(:insert) do subject.insert('rspec', {id: 1, name: 'foo'}) end it 'works' do expected(insert, 1) end end context 'when Array' do let(:insert) do subject.insert('rspec', [{id: 1, name: 'foo'}, {id: 1, name: 'foo'}]) end it 'works' do expected(insert, 2) end end end context 'when body', if: ruby_version_lt('3') do context 'when Hash' do let(:insert) do subject.insert('rspec', {id: 1, name: 'foo'}, {}) end it 'works' do expected(insert, 1) end end context 'when Array' do let(:insert) do subject.insert('rspec', [{id: 1, name: 'foo'}, {id: 1, name: 'foo'}], {}) end it 'works' do expected(insert, 2) end end end context 'when block with columns' do let(:insert) do subject.insert('rspec', columns: %i[name id], values: [['Sun', 1], ['Moon', 2]]) end it 'works' do expected(insert, 2) end context 'when string format' do let(:insert) do subject.insert('rspec', columns: %i[name id], values: [%w[Sun 1], %w[Moon 2]], format: 'JSONCompactStringsEachRow') end it 'works' do expected(insert, 2) end end end context 'when argument with columns' do let(:insert) do subject.insert('rspec', columns: %i[name id]) do |buffer| buffer << ['Sun', 1] buffer << ['Moon', 2] end end it 'works' do expected(insert, 2) end end context 'when hash with argument' do let(:insert) do subject.insert('rspec', values: [{ name: 'Sun', id: 1 }, { name: 'Moon', id: 2 }]) end it 'works' do expected(insert, 2) end end context 'when hash with block' do let(:insert) do subject.insert('rspec') do |buffer| buffer << { name: 'Sun', id: 1 } buffer << { name: 'Moon', id: 2 } end end it 'works' do expected(insert, 2) end end end ================================================ FILE: spec/click_house/extend/connection_selective_spec.rb ================================================ RSpec.describe ClickHouse::Extend::ConnectionSelective do subject do ClickHouse.connection end describe '#select_value' do context 'when exists' do it 'works' do expect(subject.select_value('SELECT 13')).to eq(13) end end context 'when not exists' do it 'works' do expect(subject.select_value('SELECT null')).to eq(nil) end end context 'when multiple columns' do it 'works' do expect(subject.select_value('SELECT 1, 2, 3, 4, 5')).to eq(1) end end end describe '#select_one' do context 'when exists' do it 'works' do expect(subject.select_one('SELECT 1 AS foo, 2 AS bar')).to eq({ 'foo' => 1, 'bar' => 2 }) end end context 'when not exists' do it 'works' do expect(subject.select_one('SELECT NULL')).to eq({ 'NULL' => nil }) end end end describe '#select_all' do before do subject.execute <<~SQL CREATE TABLE rspec (date Date, id UInt32) ENGINE TinyLog SQL subject.execute <<~SQL INSERT INTO rspec (date, id) VALUES('2000-01-01', 1), ('2000-01-02', 2) SQL end context 'when empty' do it 'works' do expect(subject.select_all('SELECT * FROM rspec where id = 100').to_a).to eq([]) end end context 'when exists' do let(:expectation) do [ { 'date' => Date.new(2000, 1, 1), 'id' => 1 }, { 'date' => Date.new(2000, 1, 2), 'id' => 2 } ] end it 'works' do expect(subject.select_all('SELECT * FROM rspec').to_a).to match_array(expectation) end end end end ================================================ FILE: spec/click_house/extend/connection_table_spec.rb ================================================ RSpec.describe ClickHouse::Extend::ConnectionTable do subject do ClickHouse.connection end describe '#tables' do context 'when empty' do it 'works' do expect(subject.tables).to eq([]) end end context 'when exists' do before do subject.execute <<~SQL CREATE TABLE rspec (date Date, id UInt32) ENGINE TinyLog SQL end it 'works' do expect(subject.tables).to contain_exactly('rspec') end end end describe '#table_exists?' do context 'when not exists' do it 'works' do expect(subject.table_exists?('foo')).to eq(false) end end context 'when exists' do before do subject.execute <<~SQL CREATE TABLE rspec (date Date, id UInt32) ENGINE TinyLog SQL end it 'works' do expect(subject.table_exists?('rspec')).to eq(true) end end end describe '#describe_table' do context 'when nested' do before do subject.execute <<~SQL CREATE TABLE rspec ( date Date, id UInt32, json Nested (uid UInt32) ) ENGINE TinyLog SQL end let(:expectation) do [ {'name' =>'date', 'type' =>'Date', 'default_type' =>'', 'default_expression' =>'', 'comment' =>'', 'codec_expression' =>'', 'ttl_expression' =>''}, {'name' =>'id', 'type' =>'UInt32', 'default_type' =>'', 'default_expression' =>'', 'comment' =>'', 'codec_expression' =>'', 'ttl_expression' =>''}, {'name' =>'json.uid', 'type' =>'Array(UInt32)', 'default_type' =>'', 'default_expression' =>'', 'comment' =>'', 'codec_expression' =>'', 'ttl_expression' =>''} ] end it 'works' do expect(subject.describe_table('rspec').to_a).to eq(expectation) end end context 'when table not exists' do it 'errors' do expect { subject.describe_table('foo') }.to raise_error(ClickHouse::DbException) end end end describe '#drop_table' do context 'when not exists' do it 'errors' do expect { subject.drop_table('foo') }.to raise_error(ClickHouse::DbException) end end context 'when if exists' do it 'works' do expect(subject.drop_table('foo', if_exists: true)).to eq(true) end end context 'when default' do before do subject.execute <<~SQL CREATE TABLE rspec (date Date, id UInt32) ENGINE TinyLog SQL end it 'works' do expect(subject.drop_table('rspec')).to eq(true) end end end describe '#truncate_table' do context 'when table exists' do before do subject.execute <<~SQL CREATE TABLE rspec(id Int64) ENGINE TinyLog SQL subject.insert('rspec', columns: %i[id], values: [[1]]) end it 'works' do expect { subject.truncate_table('rspec') }.to change { subject.select_value('SELECT COUNT(*) from rspec') }.from(1).to(0) end end context 'when table not exists' do it 'errors' do expect { subject.truncate_table('rspec') }.to raise_error(ClickHouse::DbException) end end context 'when if exists' do it 'works' do expect(subject.truncate_table('rspec', if_exists: true)).to eq(true) end end end describe '#truncate_tables' do before do subject.execute <<~SQL CREATE TABLE rspec_1(id Int64) ENGINE TinyLog SQL subject.execute <<~SQL CREATE TABLE rspec_2(id Int64) ENGINE TinyLog SQL subject.insert('rspec_1', columns: %i[id], values: [[1]]) subject.insert('rspec_2', columns: %i[id], values: [[1]]) end it 'works' do sql = <<~SQL SELECT (SELECT COUNT(*) FROM rspec_1) + (SELECT COUNT(*) FROM rspec_2) SQL expect { subject.truncate_tables }.to change { subject.select_value(sql) }.from(2).to(0) end end describe '#rename_table' do before do subject.execute <<~SQL CREATE TABLE bar(id Int64) ENGINE TinyLog SQL subject.execute <<~SQL CREATE TABLE foo(id Int64) ENGINE TinyLog SQL end context 'when 1 to 1' do it 'works' do expect { subject.rename_table('bar', 'baz') }.to change { subject.tables }.from(%w[bar foo]).to(%w[baz foo]) end end context 'when many to many' do it 'works' do expect { subject.rename_table(%w[bar foo], %w[baz foz]) }.to change { subject.tables }.from(%w[bar foo]).to(%w[baz foz]) end end context 'when incorrect arity' do it 'errors' do expect { subject.rename_table(%w[bar foo], %w[baz]) }.to raise_error(ClickHouse::StatementException) end end end describe '#create_table' do context 'when column options' do before do subject.create_table('rspec', engine: 'MergeTree()', order: 'date') do |t| t.UInt16 :id, 16, default: 0, ttl: 'date + INTERVAL 1 DAY' t.UInt16 :year t.IPv4 :ipv4 t.IPv6 :ipv6 t.Date :date t.DateTime :time, 'UTC' t.DateTime64 :time_with_usec, 4, 'UTC' t.Decimal :money, 5, 4 t.String :event, nullable: true t.Nested :json do |n| n.UInt8 :cid n.Date :created_at end t << "vendor Enum('microsoft' = 1, 'apple' = 2)" end end let(:columns) do subject.describe_table('rspec').each_with_object({}) do |column, object| object[column.fetch('name')] = column end end it 'works' do expect(columns.fetch('id')).to include('type' => 'UInt16', 'default_expression' => '0', 'ttl_expression' => 'date + toIntervalDay(1)') expect(columns.fetch('year')).to include('type' => 'UInt16', 'default_expression' => '', 'ttl_expression' => '') expect(columns.fetch('ipv4')).to include('type' => 'IPv4', 'default_expression' => '', 'ttl_expression' => '') expect(columns.fetch('ipv6')).to include('type' => 'IPv6', 'default_expression' => '', 'ttl_expression' => '') expect(columns.fetch('date')).to include('type' => 'Date', 'default_expression' => '', 'ttl_expression' => '') expect(columns.fetch('time')).to include('type' => "DateTime('UTC')", 'default_expression' => '', 'ttl_expression' => '') expect(columns.fetch('time_with_usec')).to include('type' => "DateTime64(4, 'UTC')", 'default_expression' => '', 'ttl_expression' => '') expect(columns.fetch('money')).to include('type' => 'Decimal(5, 4)', 'default_expression' => '', 'ttl_expression' => '') expect(columns.fetch('event')).to include('type' => 'Nullable(String)', 'default_expression' => '', 'ttl_expression' => '') expect(columns.fetch('json.cid')).to include('type' => 'Array(UInt8)', 'default_expression' => '', 'ttl_expression' => '') expect(columns.fetch('json.created_at')).to include('type' => 'Array(Date)', 'default_expression' => '', 'ttl_expression' => '') expect(columns.fetch('vendor')).to include('type' => "Enum8('microsoft' = 1, 'apple' = 2)") end end context 'when table options' do before do subject.create_table('rspec', order: 'year', ttl: 'date + INTERVAL 1 DAY', sample: 'year', settings: 'index_granularity=8192', primary_key: 'year', engine: 'MergeTree()') do |t| t.UInt16 :year t.Date :date end end let(:schema) do subject.execute('SHOW CREATE rspec FORMAT TabSeparatedRaw').body end let(:expectation) do <<~SQL CREATE TABLE click_house_rspec.rspec ( `year` UInt16, `date` Date ) ENGINE = MergeTree PRIMARY KEY year ORDER BY year SAMPLE BY year TTL date + toIntervalDay(1) SETTINGS index_granularity = 8192 SQL end it 'works' do expect(ClickHouse::Util::Pretty.squish(schema)).to eq(ClickHouse::Util::Pretty.squish(expectation)) end end end end ================================================ FILE: spec/click_house/integration/array_spec.rb ================================================ RSpec.describe ClickHouse::Type::ArrayType do subject do ClickHouse.connection end describe 'cast' do context 'many flat' do before do subject.execute <<~SQL CREATE TABLE rspec( a Array(DateTime), b Array(Nullable(DateTime)), c Array(DateTime64(3)), d Array(Nullable(DateTime64(3))), e Array(DateTime64(3, 'UTC')), f Array(Nullable(DateTime64(3, 'UTC'))), g Array(Decimal(10,2)), h Array(String) ) ENGINE Memory SQL subject.execute <<~SQL insert into rspec values ( array(now()), array(now()), array(now()), array(now()), array(now()), array(now()), array(5.99), array('foo') ); SQL end it 'works' do got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a').first).to be_a(Time) expect(got.fetch('b').first).to be_a(Time) expect(got.fetch('c').first).to be_a(Time) expect(got.fetch('d').first).to be_a(Time) expect(got.fetch('e').first).to be_a(Time) expect(got.fetch('f').first).to be_a(Time) expect(got.fetch('g').first).to be_a(BigDecimal) expect(got.fetch('h')).to eq(['foo']) end end context 'many nested' do before do subject.execute <<~SQL CREATE TABLE rspec( a Array(Array(DateTime)), b Array(Array(Nullable(DateTime))), c Array(Array(Array(DateTime64(3)))), d Array(Array(Array(Array(Nullable(DateTime64(3)))))), e Array(Array(Array(Array(Array(DateTime64(3, 'UTC')))))), f Array(Array(Array(Array(Array(Array(Nullable(DateTime64(3, 'UTC')))))))), g Array(Array(Array(Array(Array(Array(Array(Decimal(10,2)))))))) ) ENGINE TinyLog SQL subject.execute <<~SQL insert into rspec values ( array(array(now())), array(array((now()))), array(array(array(((now()))))), array(array(array(array((((now()))))))), array(array(array(array(array((((now())))))))), array(array(array(array(array(array((((now()))))))))), array(array(array(array(array(array(array((((5.99)))))))))) ); SQL end it 'works' do got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a').dig(0, 0)).to be_a(Time) expect(got.fetch('b').dig(0, 0)).to be_a(Time) expect(got.fetch('c').dig(0, 0, 0)).to be_a(Time) expect(got.fetch('d').dig(0, 0, 0, 0)).to be_a(Time) expect(got.fetch('e').dig(0, 0, 0, 0, 0)).to be_a(Time) expect(got.fetch('f').dig(0, 0, 0, 0, 0, 0)).to be_a(Time) expect(got.fetch('g').dig(0, 0, 0, 0, 0, 0, 0)).to be_a(BigDecimal) end end end describe 'serialize' do before do subject.execute <<~SQL CREATE TABLE rspec( a Array(Bool), b Array(DateTime64(9, 'Europe/Kyiv')), c Array(Array(UInt8)), d Array(Array(Array(Nullable(UInt8)))), e Array(String), ) ENGINE Memory SQL end let(:row) do { 'a' => [true], 'b' => [Time.find_zone("Europe/Kyiv").now], 'c' => [[1]], 'd' => [[[nil]]], 'e' => ['foo'] } end it 'works' do subject.insert('rspec', subject.table_schema('rspec').serialize_one(row)) got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a')).to eq(row.fetch('a')) expect(got.fetch('b')).to eq(row.fetch('b')) expect(got.fetch('c')).to eq(row.fetch('c')) expect(got.fetch('d')).to eq(row.fetch('d')) expect(got.fetch('e')).to eq(row.fetch('e')) end end end ================================================ FILE: spec/click_house/integration/boolean_type_spec.rb ================================================ # frozen_string_literal: true RSpec.describe ClickHouse::Type::BooleanType do subject do ClickHouse.connection end before do subject.execute <<~SQL CREATE TABLE rspec( a Boolean, b Boolean, c Boolean, d Boolean, e Nullable(Boolean) ) ENGINE Memory SQL end describe 'cast' do before do subject.execute <<~SQL INSERT INTO rspec VALUES ( 1, 0, true, false, NULL ); SQL end it 'works' do got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a')).to be_a(TrueClass) expect(got.fetch('b')).to be_a(FalseClass) expect(got.fetch('c')).to be_a(TrueClass) expect(got.fetch('b')).to be_a(FalseClass) expect(got.fetch('e')).to be_a(NilClass) end end describe 'serialize' do let(:row) do { 'a' => true, 'b' => false, 'c' => 1, 'd' => 0, 'e' => nil } end it 'works' do subject.insert('rspec', subject.table_schema('rspec').serialize_one(row)) got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a')).to be_a(TrueClass) expect(got.fetch('b')).to be_a(FalseClass) expect(got.fetch('c')).to be_a(TrueClass) expect(got.fetch('b')).to be_a(FalseClass) expect(got.fetch('e')).to be_a(NilClass) end end end ================================================ FILE: spec/click_house/integration/date_spec.rb ================================================ # frozen_string_literal: true RSpec.describe ClickHouse::Type::IPType do subject do ClickHouse.connection end before do subject.execute <<~SQL CREATE TABLE rspec( a Date, b Nullable(Date) ) ENGINE Memory SQL end describe 'cast' do before do subject.execute <<~SQL INSERT INTO rspec VALUES ( '2022-01-02', NULL ); SQL end it 'works' do got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a')).to eq(Date.new(2022, 1, 2)) expect(got.fetch('b')).to eq(nil) end end describe 'serialize' do let(:row) do { 'a' => Date.new(2022, 1, 2), 'b' => nil } end it 'works' do subject.insert('rspec', subject.table_schema('rspec').serialize_one(row)) got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a')).to eq(row.fetch('a')) expect(got.fetch('b')).to eq(row.fetch('b')) end end end ================================================ FILE: spec/click_house/integration/date_time64_spec.rb ================================================ RSpec.describe ClickHouse::Type::DateTime64Type do subject do ClickHouse.connection end before do subject.execute <<~SQL CREATE TABLE rspec( a DateTime64(0), b DateTime64(9, 'Europe/Kyiv'), c Nullable(DateTime64(9)), d Nullable(DateTime64(9, 'Europe/Kyiv')), e Nullable(DateTime64(9)), ) ENGINE Memory SQL end describe 'cast' do before do subject.execute <<~SQL INSERT INTO rspec values ( now(), now(), now(), now(), null ); SQL end it 'works' do got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a')).to be_a(Time) expect(got.fetch('a')).to have_attributes(zone: Time.now.zone) expect(got.fetch('b')).to be_a(Time) expect(got.fetch('b')).to have_attributes(zone: Time.find_zone('Europe/Kyiv').tzinfo.abbr) expect(got.fetch('c')).to be_a(Time) expect(got.fetch('d')).to be_a(Time) expect(got.fetch('d')).to have_attributes(zone: Time.find_zone('Europe/Kyiv').tzinfo.abbr) expect(got.fetch('e')).to be_a(NilClass) end end describe 'serialize' do let(:row) do { 'a' => Time.now.round, 'b' => Time.find_zone("Europe/Kyiv").now, 'c' => Time.now, 'd' => nil, 'e' => nil, } end it 'works' do subject.insert('rspec', subject.table_schema('rspec').serialize_one(row)) got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a')).to eq(row.fetch('a')) expect(got.fetch('b')).to eq(row.fetch('b')) expect(got.fetch('c')).to eq(row.fetch('c')) expect(got.fetch('d')).to eq(row.fetch('d')) expect(got.fetch('e')).to eq(row.fetch('e')) end end end ================================================ FILE: spec/click_house/integration/date_time_spec.rb ================================================ RSpec.describe ClickHouse::Type::DateTimeType do subject do ClickHouse.connection end before do subject.execute <<~SQL CREATE TABLE rspec( a DateTime, b DateTime('Europe/Kyiv'), c Nullable(DateTime), d Nullable(DateTime('Europe/Kyiv')), e Nullable(DateTime), ) ENGINE Memory SQL end describe 'cast' do before do subject.execute <<~SQL INSERT INTO rspec VALUES ( now(), now(), now(), now(), null ); SQL end it 'works' do got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a')).to be_a(Time) expect(got.fetch('a')).to have_attributes(zone: Time.now.zone) expect(got.fetch('b')).to be_a(Time) expect(got.fetch('b')).to have_attributes(zone: Time.find_zone('Europe/Kyiv').tzinfo.abbr) expect(got.fetch('c')).to be_a(Time) expect(got.fetch('d')).to be_a(Time) expect(got.fetch('d')).to have_attributes(zone: Time.find_zone('Europe/Kyiv').tzinfo.abbr) expect(got.fetch('e')).to be_a(NilClass) end end describe 'serialize' do let(:row) do { 'a' => Time.now.round, 'b' => Time.find_zone("Europe/Kyiv").now.round, 'c' => Time.now.round, 'd' => nil, 'e' => nil, } end it 'works' do subject.insert('rspec', subject.table_schema('rspec').serialize_one(row)) got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a')).to eq(row.fetch('a')) expect(got.fetch('b')).to eq(row.fetch('b')) expect(got.fetch('c')).to eq(row.fetch('c')) expect(got.fetch('d')).to eq(nil) expect(got.fetch('e')).to eq(nil) end end end ================================================ FILE: spec/click_house/integration/decimal_spec.rb ================================================ RSpec.describe ClickHouse::Type::DecimalType do subject do ClickHouse.connection end before do subject.execute <<~SQL CREATE TABLE rspec( a Decimal(10,10), b Decimal32(1), c Decimal64(10), d Decimal128(20), e Nullable(Decimal256(30)) ) ENGINE Memory SQL end describe 'cast' do before do subject.execute <<~SQL INSERT INTO rspec VALUES ( 0.1, 1/3, 1/3, 1/3, 1/3 ); SQL end it 'works' do got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a')).to be_a(BigDecimal) expect(got.fetch('b')).to be_a(BigDecimal) expect(got.fetch('c')).to be_a(BigDecimal) expect(got.fetch('d')).to be_a(BigDecimal) expect(got.fetch('e')).to be_a(BigDecimal) end it 'works with correct precision', if: ruby_version_gt('2.8') do got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a').precision).to eq(1) expect(got.fetch('b').precision).to eq(1) expect(got.fetch('c').precision).to eq(10) expect(got.fetch('d').precision).to eq(20) expect(got.fetch('e').precision).to eq(30) end end describe 'serialize' do let(:row) do { 'a' => "0.1", 'b' => BigDecimal(1), 'c' => 1.fdiv(3), 'd' => 1, 'e' => nil } end it 'works' do subject.insert('rspec', subject.table_schema('rspec').serialize_one(row)) got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a')).to eq(BigDecimal("0.1")) expect(got.fetch('b')).to eq(BigDecimal(1)) expect(got.fetch('c')).to eq(BigDecimal(1.fdiv(3), 10)) expect(got.fetch('d')).to eq(BigDecimal(1)) expect(got.fetch('e')).to eq(nil) end end end ================================================ FILE: spec/click_house/integration/enum_spec.rb ================================================ # frozen_string_literal: true RSpec.describe 'Enum' do subject do ClickHouse.connection end before do subject.execute <<~SQL CREATE TABLE rspec( a Enum('foo' = 1, 'bar' = 2), b Nullable(Enum('foo' = 1, 'bar' = 2)) ) ENGINE Memory SQL end describe 'cast' do before do subject.execute <<~SQL INSERT INTO rspec VALUES ( 1, NULL ) SQL end it 'works' do got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a')).to eq('foo') expect(got.fetch('b')).to eq(nil) end end describe 'serialize' do let(:row) do { 'a' => 'foo', 'b' => nil, } end it 'works' do subject.insert('rspec', subject.table_schema('rspec').serialize_one(row)) got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a')).to eq('foo') expect(got.fetch('b')).to eq(nil) end end end ================================================ FILE: spec/click_house/integration/float_spec.rb ================================================ # frozen_string_literal: true RSpec.describe ClickHouse::Type::FloatType do subject do ClickHouse.connection end before do subject.execute <<~SQL CREATE TABLE rspec( a Float32, b Float64, c Nullable(Float64) ) ENGINE Memory SQL end describe 'cast' do before do subject.execute <<~SQL INSERT INTO rspec VALUES ( 1.1, 2.2, NULL ); SQL end it 'works' do got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a')).to eq(1.1) expect(got.fetch('b')).to eq(2.2) expect(got.fetch('c')).to eq(nil) end end describe 'serialize' do let(:row) do { 'a' => 1.1, 'b' => 2.2, 'c' => nil, } end it 'works' do subject.insert('rspec', subject.table_schema('rspec').serialize_one(row)) got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a')).to eq(row.fetch('a')) expect(got.fetch('b')).to eq(row.fetch('b')) expect(got.fetch('c')).to eq(row.fetch('c')) end end end ================================================ FILE: spec/click_house/integration/formats.rb ================================================ # frozen_string_literal: true RSpec.describe ClickHouse::Extend::ConnectionSelective do subject do ClickHouse.connection end context 'when RowBinary' do let(:query) do 'SELECT 1 FORMAT RowBinary' end it '#select_one' do got = subject.select_one(query) expect(got).to eq("\u0001") end it '#select_value' do got = subject.select_value(query) expect(got).to eq("\u0001") end it '#summary' do got = subject.select_all(query) expect(got.summary.read_rows).to eq(1) end it '#types' do got = subject.select_all(query) expect(got.types).to eq([]) end end context 'when CSV' do let(:query) do 'SELECT 1 FORMAT CSV' end it '#select_one' do got = subject.select_one(query) expect(got).to eq(['1']) end it '#select_value' do got = subject.select_value(query) expect(got).to eq('1') end it '#summary' do got = subject.select_all(query) expect(got.summary.read_rows).to eq(1) end it '#types' do got = subject.select_all(query) expect(got.types).to eq([]) end end end ================================================ FILE: spec/click_house/integration/function_spec.rb ================================================ RSpec.describe 'Functions' do subject do ClickHouse.connection end let(:expectations) do { 'select NOW()' => Time, 'select 1 + 1' => Integer, 'select 1 * 1.0' => Float, 'select empty([])' => Integer, 'select 1 > 0' => Integer, } end it 'works' do expectations.each do |query, klass| expect(subject.select_value(query)).to be_a(klass) end end end ================================================ FILE: spec/click_house/integration/integer_spec.rb ================================================ # frozen_string_literal: true RSpec.describe ClickHouse::Type::IntegerType do subject do ClickHouse.connection end before do subject.execute <<~SQL CREATE TABLE rspec( a UInt8, b UInt16, c UInt32, d UInt64, e Int8, f Int16, g Int32, h Int64, k Nullable(Int64) ) ENGINE Memory SQL end describe 'cast' do before do subject.execute <<~SQL INSERT INTO rspec VALUES ( 1, 2, 3, 4, 5, 6, 7, 8, NULL ); SQL end it 'works' do got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a')).to eq(1) expect(got.fetch('b')).to eq(2) expect(got.fetch('c')).to eq(3) expect(got.fetch('d')).to eq(4) expect(got.fetch('e')).to eq(5) expect(got.fetch('f')).to eq(6) expect(got.fetch('g')).to eq(7) expect(got.fetch('h')).to eq(8) expect(got.fetch('k')).to eq(nil) end end describe 'serialize' do let(:row) do { 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5, 'f' => 6, 'g' => 7, 'h' => 8, 'k' => nil, } end it 'works' do subject.insert('rspec', subject.table_schema('rspec').serialize_one(row)) got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a')).to eq(row.fetch('a')) expect(got.fetch('b')).to eq(row.fetch('b')) expect(got.fetch('c')).to eq(row.fetch('c')) expect(got.fetch('d')).to eq(row.fetch('d')) expect(got.fetch('e')).to eq(row.fetch('e')) expect(got.fetch('f')).to eq(row.fetch('f')) expect(got.fetch('g')).to eq(row.fetch('g')) expect(got.fetch('h')).to eq(row.fetch('h')) expect(got.fetch('k')).to eq(row.fetch('k')) end end end ================================================ FILE: spec/click_house/integration/ip_spec.rb ================================================ # frozen_string_literal: true RSpec.describe ClickHouse::Type::IPType do subject do ClickHouse.connection end before do subject.execute <<~SQL CREATE TABLE rspec( a IPv4, b Nullable(IPv4), c IPv6, d Nullable(IPv6) ) ENGINE Memory SQL end describe 'cast' do before do subject.execute <<~SQL INSERT INTO rspec VALUES ( '127.0.0.1', '127.0.0.1', '::1', NULL ); SQL end it 'works' do got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a')).to eq(IPAddr.new('127.0.0.1')) expect(got.fetch('b')).to eq(IPAddr.new('127.0.0.1')) expect(got.fetch('c')).to eq(IPAddr.new('::1')) expect(got.fetch('d')).to eq(nil) end end describe 'serialize' do let(:row) do { 'a' => IPAddr.new('127.0.0.1'), 'b' => '127.0.0.1', # as string 'c' => IPAddr.new('::1'), 'd' => nil } end it 'works' do subject.insert('rspec', subject.table_schema('rspec').serialize_one(row)) got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a')).to eq(row.fetch('a')) expect(got.fetch('b')).to eq(IPAddr.new(row.fetch('b'))) expect(got.fetch('c')).to eq(row.fetch('c')) expect(got.fetch('d')).to eq(row.fetch('d')) end end end ================================================ FILE: spec/click_house/integration/loggin_spec.rb ================================================ RSpec.describe ClickHouse::Middleware::Logging do subject do ClickHouse::Connection.new(ClickHouse.config.clone.assign(logger: logger)) end let(:out) do StringIO.new end let(:logger) do Logger.new(out) end context 'when POST' do it 'works' do subject.execute('SELECT 1') expect(out.string).to match(/Total: \d/) expect(out.string).to include('SELECT 1;') expect(out.string).to include('Read: 1 rows') expect(out.string).to include('Written: 0 rows') end end context 'when GET' do it 'works' do subject.select_all('SELECT 1') expect(out.string).to match(/Total: \d/) expect(out.string).to include('SELECT 1;') expect(out.string).to include('Read: 1 rows') expect(out.string).to include('Written: 0 rows') end end end ================================================ FILE: spec/click_house/integration/low_cardinality_spec.rb ================================================ RSpec.describe ClickHouse::Type::LowCardinalityType do subject do ClickHouse.connection end before do subject.execute <<~SQL CREATE TABLE rspec( a LowCardinality(DateTime), b LowCardinality(DateTime('Europe/Kyiv')), c LowCardinality(Nullable(String)) ) ENGINE Memory SQL end describe 'cast' do before do subject.execute <<~SQL INSERT INTO rspec VALUES ( now(), now(), null ); SQL end it 'works' do got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a')).to be_a(Time) expect(got.fetch('a')).to have_attributes(zone: Time.now.zone) expect(got.fetch('b')).to be_a(Time) expect(got.fetch('b')).to have_attributes(zone: Time.find_zone('Europe/Kyiv').tzinfo.abbr) expect(got.fetch('c')).to be_a(NilClass) end end describe 'serialize' do let(:row) do { 'a' => Time.now, 'b' => Time.now, 'c' => nil, } end it 'works' do subject.insert('rspec', subject.table_schema('rspec').serialize_one(row)) got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a')).to be_a(Time) expect(got.fetch('b')).to be_a(Time) expect(got.fetch('c')).to be_a(NilClass) end end end ================================================ FILE: spec/click_house/integration/map_spec.rb ================================================ # frozen_string_literal: true RSpec.describe ClickHouse::Type::MapType do subject do ClickHouse.connection end before do subject.execute <<~SQL CREATE TABLE rspec( a Map(LowCardinality(String), Array(DateTime('Europe/Kyiv'))), b Map(Int8, IPv4) ) ENGINE Memory SQL end describe 'cast' do before do subject.execute <<~SQL INSERT INTO rspec VALUES ( {'foo': ['2019-01-01']}, {1: '127.0.0.1'} ) SQL end it 'works' do got = subject.select_one('SELECT * FROM rspec') expect(got.dig('a', 'foo')).to eq([Time.find_zone('Europe/Kyiv').parse('2019-01-01')]) expect(got.dig('b', 1)).to eq(IPAddr.new('127.0.0.1')) end end describe 'serialize' do let(:row) do { 'a' => {'foo' => [Time.find_zone('Europe/Kyiv').now.round]}, 'b' => { 1 => IPAddr.new('127.0.0.1')}, } end it 'works' do subject.insert('rspec', subject.table_schema('rspec').serialize_one(row)) got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a')).to eq(row.fetch('a')) expect(got.fetch('b')).to eq(row.fetch('b')) end end end ================================================ FILE: spec/click_house/integration/nested_spec.rb ================================================ # frozen_string_literal: true RSpec.describe 'Nested' do subject do ClickHouse.connection end before do subject.execute <<~SQL CREATE TABLE rspec( json Nested( a Date ) ) ENGINE Memory SQL end describe 'cast' do before do subject.execute <<~SQL INSERT INTO rspec VALUES ( (['2022-01-01']) ) SQL end it 'works' do got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('json.a')).to eq([Date.new(2022, 1, 1)]) end end describe 'serialize' do let(:row) do { 'json.a' => [Date.new(2022, 1, 2)] } end it 'works' do subject.insert('rspec', subject.table_schema('rspec').serialize_one(row)) got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('json.a')).to eq(row.fetch('json.a')) end end end ================================================ FILE: spec/click_house/integration/string_spec.rb ================================================ # frozen_string_literal: true RSpec.describe ClickHouse::Type::StringType do subject do ClickHouse.connection end before do subject.execute <<~SQL CREATE TABLE rspec( a String, b FixedString(2), c UUID, d Nullable(UUID) ) ENGINE Memory SQL end describe 'cast' do before do subject.execute <<~SQL INSERT INTO rspec VALUES ( 'x', 'y', 'da70495b-1ff7-49e5-8feb-d657bd4ea1ea', NULL ); SQL end it 'works' do got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a')).to eq("x") expect(got.fetch('b')).to eq("y\u0000") expect(got.fetch('c')).to eq("da70495b-1ff7-49e5-8feb-d657bd4ea1ea") expect(got.fetch('d')).to eq(nil) end end describe 'serialize' do let(:row) do { 'a' => 'foo', 'b' => 'xe', 'c' => "da70495b-1ff7-49e5-8feb-d657bd4ea1ea", 'd' => nil } end it 'works' do subject.insert('rspec', subject.table_schema('rspec').serialize_one(row)) got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a')).to eq(row.fetch('a')) expect(got.fetch('b')).to eq(row.fetch('b')) expect(got.fetch('c')).to eq(row.fetch('c')) expect(got.fetch('d')).to eq(row.fetch('d')) end end end ================================================ FILE: spec/click_house/integration/symbolize_keys_spec.rb ================================================ # frozen_string_literal: true RSpec.describe ClickHouse::Config do subject do ClickHouse::Connection.new(ClickHouse.config.clone.assign(symbolize_keys: true)) end before do subject.execute <<~SQL CREATE TABLE rspec( a String, ) ENGINE Memory SQL end describe 'cast' do before do subject.execute <<~SQL INSERT INTO rspec VALUES ( 'foo' ); SQL end it 'works' do got = subject.select_one('SELECT * FROM rspec') expect(got).to eq({a: "foo"}) got = subject.select_value('SELECT * FROM rspec') expect(got).to eq("foo") got = subject.select_all('SELECT * FROM rspec') expect(got.meta).to eq([{:name=>"a", :type=>"String"}]) expect(got.statistics).to include(rows_read: 1) end end describe 'serialize' do let(:row) do { a: 'foo' } end it 'works' do subject.insert_rows('rspec', subject.table_schema('rspec').serialize_one(row)) got = subject.select_one('SELECT * FROM rspec') expect(got.fetch(:a)).to eq(row.fetch(:a)) end end end ================================================ FILE: spec/click_house/integration/table_schema_spec.rb ================================================ RSpec.describe ClickHouse::Response::ResultSet do subject do ClickHouse.connection end before do subject.execute <<~SQL CREATE TABLE rspec( a Boolean, b Array(Nullable(String)) ) ENGINE Memory SQL end let(:schema) do subject.table_schema('rspec') end describe '#types' do it 'works' do expect(schema.types).to have_key('a') expect(schema.types).to have_key('b') end end describe '#serialize_column' do it 'works' do expect(schema.serialize_column('a', true)).to eq(1) expect(schema.serialize_column('b', [])).to eq([]) end it 'errors if column missing' do expect { schema.serialize_column('foo', 'bar') }.to raise_error(ClickHouse::SerializeError) end it 'errors if value has improper type' do expect { schema.serialize_column('b', nil) }.to raise_error(ClickHouse::SerializeError) end end describe '#serialize_one' do it 'works' do expect(schema.serialize_one({'a' => true, 'b' => ['foo']})).to eq({'a' => 1, 'b' => ['foo']}) end end describe '#serialize' do let(:row) do {'a' => true, 'b' => ['foo']} end let(:expectation) do {'a' => 1, 'b' => ['foo']} end context 'when Hash' do it 'works' do expect(schema.serialize(row)).to eq(expectation) end end context 'when Array' do it 'works' do expect(schema.serialize([row])).to eq([expectation]) end end context 'when other' do it 'errors' do expect { schema.serialize(nil) }.to raise_error(ArgumentError) end end end end ================================================ FILE: spec/click_house/integration/tuple_spec.rb ================================================ # frozen_string_literal: true RSpec.describe ClickHouse::Type::TupleType do subject do ClickHouse.connection end before do subject.execute <<~SQL CREATE TABLE rspec( a Tuple(IPv4, Nullable(Date), Nullable(String)) ) ENGINE Memory SQL end describe 'cast' do before do subject.execute <<~SQL INSERT INTO rspec VALUES ( ('127.0.0.1', '2022-01-02', NULL) ); SQL end let(:expectation) do [ IPAddr.new('127.0.0.1'), Date.new(2022, 1, 2), nil ] end it 'cast type' do got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a')).to eq(expectation) end end describe 'serialize' do let(:row) do { 'a' => [ IPAddr.new('127.0.0.1'), Date.new(2022, 1, 1), nil ], } end it 'works' do subject.insert('rspec', subject.table_schema('rspec').serialize_one(row)) got = subject.select_one('SELECT * FROM rspec') expect(got.fetch('a')).to eq(row.fetch('a')) end end end ================================================ FILE: spec/click_house/response/factory_spec.rb ================================================ RSpec.describe ClickHouse::Response::Factory do subject do ClickHouse.connection end describe 'WITH totals modifier' do context 'when blank' do let(:response) do subject.select_all('SELECT 1') end it 'is empty' do expect(response.totals).to eq(nil) end end context 'when exists' do let(:response) do subject.select_all('SELECT SUM(1) AS s WITH TOTALS') end it 'is present' do expect(response.totals).to eq({ 's' => '1' }) end end end end ================================================ FILE: spec/click_house/type/date_time64_spec.rb ================================================ RSpec.describe ClickHouse::Type::DateTime64Type do let(:precisions) do (0..9).to_a end describe '#serialize' do let(:time) do Time.new(2019, 1, 1, 9, 5, 6) end it 'works' do precisions.each do |precision| tail = "." + "0" * precision if precision > 0 expect(subject.serialize(time, precision)).to eq("2019-01-01 09:05:06#{tail}") end end end describe '#cast' do context 'when zone is empty' do let(:time) do Time.new(2019, 1, 1, 9, 5, 6) end it 'works' do expect(subject.cast('2019-01-01 09:05:06.0000')).to eq(time) end end context 'when zone exists' do let(:time) do Time.new(2019, 1, 1, 9, 5, 6, Time.find_zone('Europe/Kyiv')) end it 'works' do expect(subject.cast('2019-01-01 09:05:06', 0, 'Europe/Kyiv').to_s).to eq(time.to_s) expect(subject.cast('2019-01-01 09:05:06.00', 9, 'Europe/Kyiv').to_s).to eq(time.to_s) end end end end ================================================ FILE: spec/click_house/type/date_time_type_spec.rb ================================================ RSpec.describe ClickHouse::Type::DateTimeType do describe '#serialize' do let(:time) do Time.new(2019, 1, 1, 9, 5, 6) end it 'works' do expect(subject.serialize(time)).to eq('2019-01-01 09:05:06') end end describe '#cast' do context 'when zone is empty' do let(:time) do Time.new(2019, 1, 1, 9, 5, 6) end it 'works' do expect(subject.cast('2019-01-01 09:05:06')).to eq(time) end end context 'when zone exists' do let(:time) do Time.new(2019, 1, 1, 9, 5, 6, Time.find_zone('Europe/Kyiv')) end it 'works' do expect(subject.cast('2019-01-01 09:05:06', 'Europe/Kyiv')).to eq(time) end end end end ================================================ FILE: spec/click_house/type/decimal_type_spec.rb ================================================ RSpec.describe ClickHouse::Type::DecimalType do describe '#casr' do context 'when String' do it 'works' do expect(subject.cast("1.0")).to eq(BigDecimal(1.0, 1)) end end context 'when BigDecimal' do it 'works' do expect(subject.cast(BigDecimal(1))).to eq(BigDecimal(1)) end end context 'when Float' do it 'works' do expect(subject.cast(1.0, 1)).to eq(BigDecimal(1.0, 1)) expect(subject.cast(1.0)).to eq(BigDecimal(1.0, ClickHouse::Type::DecimalType::MAXIMUM)) end end end end ================================================ FILE: spec/click_house/type/fixed_string_type_spec.rb ================================================ RSpec.describe ClickHouse::Type::FixedStringType do describe '#serialize' do def target(value, limit = nil) described_class.new.serialize(value, limit) end it 'works' do expect(target(nil)).to eq(nil) expect(target('foo bar')).to eq('foo bar') expect(target('foo bar', 1)).to eq('f') expect(target('foo bar', 2)).to eq('fo') end end end ================================================ FILE: spec/click_house/type/float_type_spec.rb ================================================ RSpec.describe ClickHouse::Type::FloatType do describe '#serialize' do it 'works' do expect(subject.serialize(5)).to be_a(Float) expect(subject.serialize(5)).to eq(5.0) expect(subject.serialize(5.0)).to eq(5.0) expect(subject.serialize(nil)).to eq(nil) end end describe '#cast' do it 'works' do expect(subject.cast(5)).to be_a(Float) expect(subject.cast(5)).to eq(5.0) expect(subject.cast(5.0)).to eq(5.0) expect(subject.cast(nil)).to eq(nil) end end end ================================================ FILE: spec/click_house/type/ip_type_spec.rb ================================================ RSpec.describe ClickHouse::Type::IPType do describe '#cast' do let(:ip) do IPAddr.new('127.0.0.1') end it 'works' do expect(subject.cast(String(ip))).to eq(ip) end end describe '#serialize' do let(:ip) do IPAddr.new('127.0.0.1') end it 'works' do expect(subject.serialize(ip)).to eq('127.0.0.1') end end end ================================================ FILE: spec/click_house/util/pretty_spec.rb ================================================ RSpec.describe ClickHouse::Util::Pretty do describe '#size' do let(:expectation) do { 0 => '0B', 100 => '100.0B', 1456 => '1.4KiB', 1024000 * 2 => '2.0MiB', 10737418240 => '10.0GiB', 10737418240 * 1_000 => '9.8TiB', } end it 'works' do expectation.each do |bytes, pretty| expect(described_class.size(bytes)).to eq(pretty) end end end end ================================================ FILE: spec/oj_helper.rb ================================================ # frozen_string_literal: tru RSpec.configure do |config| config.before(:each) do ClickHouse.config do |c| c.json_parser = ClickHouse::Middleware::ParseJsonOj c.json_serializer = ClickHouse::Serializer::JsonOjSerializer end end end ================================================ FILE: spec/spec_helper.rb ================================================ # frozen_string_literal: true $ROOT_PATH = File.expand_path('../../', __FILE__).freeze require 'bundler/setup' require 'click_house' require 'pry' Dir[File.join($ROOT_PATH, 'spec', 'support', '*.rb')].each { |f| require f } ClickHouse.config do |config| config.logger = Logger.new('log/test.log', level: Logger::DEBUG) config.database = 'click_house_rspec' config.url = 'http://localhost:8123?allow_suspicious_low_cardinality_types=1&output_format_arrow_low_cardinality_as_dictionary=1' end RSpec.configure do |config| config.example_status_persistence_file_path = '.rspec_status' config.disable_monkey_patching! config.filter_run :focus config.run_all_when_everything_filtered = true config.order = :random Kernel.srand config.seed config.expect_with :rspec do |c| c.syntax = :expect end end ================================================ FILE: spec/support/database_cleaner.rb ================================================ SYSTEM_DATABASES = %w[default system _temporary_and_external_tables] RSpec.configure do |config| config.around(:each) do |example| ClickHouse.connection.create_database(ClickHouse.config.database, if_not_exists: true) example.run ClickHouse.connection.databases.each do |database| next if SYSTEM_DATABASES.include?(database) ClickHouse.connection.drop_database(database, if_exists: true) end end end ================================================ FILE: spec/support/reset_connection.rb ================================================ # frozen_string_literal: tru RSpec.configure do |config| config.around(:each) do |example| ClickHouse.connection = nil example.run end end ================================================ FILE: spec/support/ruby_version.rb ================================================ # frozen_string_literal: true # @return [Boolean] # @param version [String] like "2.7" def ruby_version_gt(version) Gem::Version.new(RUBY_VERSION) > Gem::Version.new(version) end # @return [Boolean] # @param version [String] like "2.7" def ruby_version_lt(version) Gem::Version.new(RUBY_VERSION) < Gem::Version.new(version) end ================================================ FILE: tmp/.keep ================================================