pinot-client 1.29.0 → 1.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f728a6fe054a729893524d1da24e6ab2cddabda2d17c3bfae183e77271a70b1c
4
- data.tar.gz: 6f10e6f81f725d9e34ed0d68a3ed04070244308f949a7a01533f90940fff18a9
3
+ metadata.gz: cf947863b8d33ffa5c029bdd62e51f3451b76320a0b5f46c1d48ba8994827767
4
+ data.tar.gz: 433c4df6222017a3e32069410e550146ffa249ba6b34c9cb12bbd49b89933191
5
5
  SHA512:
6
- metadata.gz: 26d0a240f8d335f1acf02363ab974bf8e0323c3ddc045b2c9539340468231d68c6fe6c1c8517e0fb8a79ad4b154bd8178847e890d8475280200b33f327fcb467
7
- data.tar.gz: 987c72674f5902f79365ff493ff541299e0efcc13374016bee1c52b98018734ff50498d7991c08807f50c5e0b70aff2be734eaa564ba7aa640da6530e66b4665
6
+ metadata.gz: ada0f4344c5482daafefdf4cb062e80601723119b2732df5d5e116d1247931076db9eed6f3c393e6af3befcde14dedaac66a7a1fa90e7a0a6e8a65ce81e4eab0
7
+ data.tar.gz: fe0699c5d2fe75b1cb0aeeb832dcb57eb8b7fe44e33801171c76a7006e99cee08f34476134d1e87e2dccce8695a8dfc4b7b642436c5faebbe60246c3cbebd627
@@ -0,0 +1,105 @@
1
+ require "pinot"
2
+ require "pinot/active_support_notifications"
3
+
4
+ module Pinot
5
+ # Rails integration via Railtie — zero configuration required.
6
+ #
7
+ # Automatically activated when the gem is required inside a Rails app.
8
+ # Just add `gem "pinot-client"` to your Gemfile.
9
+ #
10
+ # == What it does
11
+ #
12
+ # 1. **ActiveSupport::Notifications bridge** — every Pinot query fires a
13
+ # "sql.pinot" event on the AS::N bus, picked up by Rails log subscribers,
14
+ # Skylight, Scout APM, etc.
15
+ #
16
+ # 2. **OpenTelemetry bridge** — when the opentelemetry-api gem is present,
17
+ # creates "pinot.query" spans and injects W3C trace-context headers into
18
+ # every outbound broker request.
19
+ #
20
+ # 3. **X-Request-Id propagation** — inserts Pinot::RequestIdMiddleware into
21
+ # the Rack stack. The current request's X-Request-Id is forwarded as an
22
+ # HTTP header on every broker call so Pinot broker logs can be correlated
23
+ # with application request logs.
24
+ #
25
+ # == Opting out of individual features
26
+ #
27
+ # # config/initializers/pinot.rb
28
+ # Rails.application.config.pinot.notifications = false
29
+ # Rails.application.config.pinot.open_telemetry = false
30
+ # Rails.application.config.pinot.request_id = false
31
+ #
32
+ # == Manual setup (non-Rails / opt-out of Railtie entirely)
33
+ #
34
+ # require "pinot/active_support_notifications"
35
+ # Pinot::ActiveSupportNotifications.install!
36
+ #
37
+ # require "pinot/open_telemetry"
38
+ # Pinot::OpenTelemetry.install!
39
+ #
40
+ # # In your Rack middleware stack:
41
+ # use Pinot::RequestIdMiddleware
42
+ class Railtie < ::Rails::Railtie
43
+ initializer "pinot.install_notifications" do |app|
44
+ opts = app.config.pinot
45
+ next if opts[:notifications] == false
46
+
47
+ require "pinot/active_support_notifications"
48
+ ActiveSupportNotifications.install!
49
+ end
50
+
51
+ initializer "pinot.install_open_telemetry" do |app|
52
+ opts = app.config.pinot
53
+ next if opts[:open_telemetry] == false
54
+
55
+ begin
56
+ require "opentelemetry"
57
+ require "pinot/open_telemetry"
58
+ OpenTelemetry.install!
59
+ rescue LoadError
60
+ # opentelemetry-api gem not present — skip silently
61
+ end
62
+ end
63
+
64
+ initializer "pinot.request_id_propagation" do |app|
65
+ opts = app.config.pinot
66
+ next if opts[:request_id] == false
67
+
68
+ app.config.middleware.use(RequestIdMiddleware)
69
+ Connection.prepend(RequestIdInjector)
70
+ end
71
+ end
72
+
73
+ # Rack middleware that captures X-Request-Id from the inbound HTTP request
74
+ # and stores it in a thread-local for the duration of the request.
75
+ #
76
+ # Inserted automatically by Pinot::Railtie. For non-Rails Rack apps:
77
+ #
78
+ # use Pinot::RequestIdMiddleware
79
+ class RequestIdMiddleware
80
+ RACK_HEADER = "HTTP_X_REQUEST_ID".freeze
81
+
82
+ def initialize(app)
83
+ @app = app
84
+ end
85
+
86
+ def call(env)
87
+ Thread.current[:pinot_request_id] = env[RACK_HEADER]
88
+ @app.call(env)
89
+ ensure
90
+ Thread.current[:pinot_request_id] = nil
91
+ end
92
+ end
93
+
94
+ # Prepended into Connection when the Railtie is active.
95
+ # Automatically merges the current request's X-Request-Id into every
96
+ # outbound Pinot query as an HTTP header, without requiring callers to
97
+ # pass it explicitly.
98
+ module RequestIdInjector
99
+ def execute_sql(table, query, query_timeout_ms: nil, headers: {})
100
+ rid = Thread.current[:pinot_request_id]
101
+ merged = rid && !headers.key?("X-Request-Id") ? headers.merge("X-Request-Id" => rid) : headers
102
+ super(table, query, query_timeout_ms: query_timeout_ms, headers: merged)
103
+ end
104
+ end
105
+ end
@@ -110,11 +110,8 @@ module Pinot
110
110
  if raw.include?(".") || raw.include?("e") || raw.include?("E")
111
111
  # Floating point string — check if it's a whole number
112
112
  bd = BigDecimal(raw)
113
- begin
114
- return 0 if bd.infinite? || bd.nan?
115
- rescue StandardError
116
- return 0
117
- end
113
+ return 0 if bd.infinite? || bd.nan?
114
+
118
115
  int_val = bd.to_i
119
116
  return 0 unless bd == BigDecimal(int_val.to_s)
120
117
  return 0 if int_val > INT32_MAX || int_val < INT32_MIN
@@ -139,11 +136,8 @@ module Pinot
139
136
  begin
140
137
  if raw.include?(".") || raw.include?("e") || raw.include?("E")
141
138
  bd = BigDecimal(raw)
142
- begin
143
- return 0 if bd.infinite? || bd.nan?
144
- rescue StandardError
145
- return 0
146
- end
139
+ return 0 if bd.infinite? || bd.nan?
140
+
147
141
  int_val = bd.to_i
148
142
  return 0 unless bd == BigDecimal(int_val.to_s)
149
143
 
@@ -0,0 +1,122 @@
1
+ require "net/http"
2
+ require "uri"
3
+ require "json"
4
+
5
+ module Pinot
6
+ # Thin client for the Pinot Controller REST API — table listing and schema
7
+ # introspection. Useful for tooling, migrations, and debugging column types.
8
+ #
9
+ # == Usage
10
+ #
11
+ # client = Pinot::SchemaClient.new("http://controller:9000")
12
+ #
13
+ # client.list_tables # => ["baseballStats", "orders", ...]
14
+ # client.get_schema("baseballStats") # => Hash (raw schema JSON)
15
+ # client.get_table_config("baseballStats")# => Hash (raw tableConfig JSON)
16
+ # client.table_exists?("orders") # => true / false
17
+ #
18
+ # == Authentication / extra headers
19
+ #
20
+ # client = Pinot::SchemaClient.new(
21
+ # "https://controller:9000",
22
+ # headers: { "Authorization" => "Bearer <token>" }
23
+ # )
24
+ #
25
+ # The client is intentionally stateless and lightweight — it uses a shared
26
+ # HttpClient (connection pool) but does not perform background polling.
27
+ class SchemaClient
28
+ TABLES_PATH = "/tables".freeze
29
+ SCHEMA_PATH = "/schemas/%<table>s".freeze
30
+ TABLE_CONFIG_PATH = "/tables/%<table>s".freeze
31
+
32
+ # @param controller_address [String] base URL e.g. "controller:9000" or
33
+ # "http://controller:9000" or "https://controller:9000"
34
+ # @param headers [Hash] extra HTTP headers for every request
35
+ # @param http_client [HttpClient, nil] optional pre-configured client
36
+ def initialize(controller_address, headers: {}, http_client: nil)
37
+ @base = normalize_address(controller_address)
38
+ @headers = { "Accept" => "application/json" }.merge(headers)
39
+ @http = http_client || HttpClient.new
40
+ end
41
+
42
+ # Returns an array of all table names known to the controller.
43
+ #
44
+ # @return [Array<String>]
45
+ def list_tables
46
+ body = get_json(TABLES_PATH)
47
+ body["tables"] || []
48
+ end
49
+
50
+ # Returns the schema for a table as a Hash.
51
+ #
52
+ # @param table [String] table name (without _OFFLINE / _REALTIME suffix)
53
+ # @return [Hash] raw schema JSON
54
+ # @raise [TableNotFoundError] if the table or schema does not exist (404)
55
+ # @raise [TransportError] on other non-200 responses
56
+ def get_schema(table)
57
+ get_json(format(SCHEMA_PATH, table: table))
58
+ end
59
+
60
+ # Returns the full table config (including segmentsConfig, tableIndexConfig,
61
+ # tenants, etc.) for a table as a Hash.
62
+ #
63
+ # @param table [String] table name (without _OFFLINE / _REALTIME suffix)
64
+ # @return [Hash] raw tableConfig JSON
65
+ # @raise [TableNotFoundError] if the table does not exist (404)
66
+ # @raise [TransportError] on other non-200 responses
67
+ def get_table_config(table)
68
+ get_json(format(TABLE_CONFIG_PATH, table: table))
69
+ end
70
+
71
+ # Returns true when the controller knows about the given table.
72
+ #
73
+ # @param table [String]
74
+ # @return [Boolean]
75
+ def table_exists?(table)
76
+ get_table_config(table)
77
+ true
78
+ rescue TableNotFoundError
79
+ false
80
+ end
81
+
82
+ # Returns the column names and their data types for a table.
83
+ #
84
+ # Convenience wrapper around get_schema that returns a flat Hash:
85
+ # { "playerId" => "INT", "playerName" => "STRING", ... }
86
+ #
87
+ # @param table [String]
88
+ # @return [Hash{String => String}]
89
+ def column_types(table)
90
+ schema = get_schema(table)
91
+ dims = schema["dimensionFieldSpecs"] || []
92
+ metrics = schema["metricFieldSpecs"] || []
93
+ date_time = schema["dateTimeFieldSpecs"] || []
94
+ (dims + metrics + date_time).to_h do |spec|
95
+ [spec["name"], spec["dataType"]]
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def normalize_address(address)
102
+ addr = address.to_s.chomp("/")
103
+ addr.start_with?("http://", "https://") ? addr : "http://#{addr}"
104
+ end
105
+
106
+ def get_json(path)
107
+ url = "#{@base}#{path}"
108
+ resp = @http.get(url, headers: @headers)
109
+
110
+ case resp.code.to_i
111
+ when 200
112
+ JSON.parse(resp.body)
113
+ when 404
114
+ raise TableNotFoundError, "not found: #{path}"
115
+ else
116
+ raise TransportError, "controller returned HTTP #{resp.code} for #{path}"
117
+ end
118
+ rescue JSON::ParserError => e
119
+ raise TransportError, "invalid JSON from controller: #{e.message}"
120
+ end
121
+ end
122
+ end
data/lib/pinot/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pinot
2
- VERSION = "1.29.0".freeze
2
+ VERSION = "1.30.0".freeze
3
3
  end
data/lib/pinot.rb CHANGED
@@ -24,6 +24,7 @@ require_relative "pinot/paginator"
24
24
  require_relative "pinot/connection"
25
25
  require_relative "pinot/prepared_statement"
26
26
  require_relative "pinot/connection_factory"
27
+ require_relative "pinot/schema_client"
27
28
 
28
29
  require_relative "pinot/grpc_config"
29
30
  begin
@@ -36,3 +37,5 @@ begin
36
37
  require_relative "pinot/zookeeper_broker_selector"
37
38
  rescue LoadError # rubocop:disable Lint/SuppressedException
38
39
  end
40
+
41
+ require_relative "pinot/railtie" if defined?(Rails::Railtie)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pinot-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.29.0
4
+ version: 1.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Xiang Fu
@@ -122,6 +122,20 @@ dependencies:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
124
  version: '3.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: bundler-audit
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.9'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '0.9'
125
139
  description: A Ruby client for Apache Pinot, mirroring the Go client API
126
140
  email:
127
141
  executables: []
@@ -151,8 +165,10 @@ files:
151
165
  - lib/pinot/proto/broker_service_pb.rb
152
166
  - lib/pinot/proto/broker_service_services_pb.rb
153
167
  - lib/pinot/query_result.rb
168
+ - lib/pinot/railtie.rb
154
169
  - lib/pinot/request.rb
155
170
  - lib/pinot/response.rb
171
+ - lib/pinot/schema_client.rb
156
172
  - lib/pinot/simple_broker_selector.rb
157
173
  - lib/pinot/table_aware_broker_selector.rb
158
174
  - lib/pinot/tls_config.rb