ksql 0.1.0.beta

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.
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ksql'
4
+
5
+ # The following example is from https://ksqldb.io/quickstart.html
6
+
7
+ # * Create a stream
8
+
9
+ Ksql::Client.ksql("CREATE STREAM riderLocations (profileId VARCHAR, latitude DOUBLE, longitude DOUBLE)
10
+ WITH (kafka_topic='locations', value_format='json', partitions=1);")
11
+
12
+ # * Create materialized views
13
+
14
+ Ksql::Client.ksql("CREATE TABLE currentLocation AS
15
+ SELECT profileId,
16
+ LATEST_BY_OFFSET(latitude) AS la,
17
+ LATEST_BY_OFFSET(longitude) AS lo
18
+ FROM riderlocations
19
+ GROUP BY profileId
20
+ EMIT CHANGES;")
21
+
22
+ Ksql::Client.ksql("CREATE TABLE ridersNearMountainView AS
23
+ SELECT ROUND(GEO_DISTANCE(la, lo, 37.4133, -122.1162), -1) AS distanceInMiles,
24
+ COLLECT_LIST(profileId) AS riders,
25
+ COUNT(*) AS count
26
+ FROM currentLocation
27
+ GROUP BY ROUND(GEO_DISTANCE(la, lo, 37.4133, -122.1162), -1);")
28
+
29
+ # * Run a push query over the stream
30
+
31
+ stream = Ksql::Client.stream('SELECT * FROM riderLocations WHERE GEO_DISTANCE(latitude, longitude, 37.4133, -122.1162) <= 5 EMIT CHANGES;')
32
+
33
+ stream.start do |location|
34
+ File.open('output.log', 'a') do |f|
35
+ f.write("Latitude: #{location.latitude}, Longitude: #{location.longitude}")
36
+ end
37
+ end
38
+
39
+ # * Populate the stream with events
40
+
41
+ Ksql::Client.ksql("INSERT INTO riderLocations (profileId, latitude, longitude) VALUES ('c2309eec', 37.7877, -122.4205);")
42
+ Ksql::Client.ksql("INSERT INTO riderLocations (profileId, latitude, longitude) VALUES ('18f4ea86', 37.3903, -122.0643);")
43
+ Ksql::Client.ksql("INSERT INTO riderLocations (profileId, latitude, longitude) VALUES ('4ab5cbad', 37.3952, -122.0813);")
44
+ Ksql::Client.ksql("INSERT INTO riderLocations (profileId, latitude, longitude) VALUES ('8b6eae59', 37.3944, -122.0813);")
45
+ Ksql::Client.ksql("INSERT INTO riderLocations (profileId, latitude, longitude) VALUES ('4a7c7b41', 37.4049, -122.0822);")
46
+ Ksql::Client.ksql("INSERT INTO riderLocations (profileId, latitude, longitude) VALUES ('4ddad000', 37.7857, -122.4011);")
47
+
48
+ # * Run a Pull query against the materialized view
49
+
50
+ Ksql::Client.query('SELECT * from ridersNearMountainView WHERE distanceInMiles <= 10;')
51
+
52
+ # * Clean up ksqlDB
53
+
54
+ stream.close
55
+
56
+ Ksql::Client.ksql('DROP TABLE IF EXISTS ridersNearMountainView;')
57
+ Ksql::Client.ksql('DROP TABLE IF EXISTS currentLocation;')
58
+ Ksql::Client.ksql('DROP STREAM IF EXISTS riderLocations;')
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ # Creates the Ksql initializer file for Rails apps.
6
+ #
7
+ # @example Invokation from terminal
8
+ # rails generate ksql
9
+ #
10
+ class KsqlGenerator < Rails::Generators::Base
11
+ desc "Description:\n This creates a Rails initializer for Ksql"
12
+
13
+ source_root File.expand_path('templates', __dir__)
14
+
15
+ desc 'Configures Ksql to connect to ksqlDB'
16
+ def generate_layout
17
+ template 'initializer.rb', 'config/initializers/ksql.rb'
18
+ end
19
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ Ksql.configure do |config|
4
+ config.host = 'http://localhost:8088' # required
5
+ # config.auth = 'user:password' # optional
6
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ksql
4
+ module Api
5
+ class CloseQuery
6
+ Headers = { 'Accept' => 'application/json' }.freeze
7
+
8
+ #
9
+ # Build the ksqlDB /close-query request
10
+ #
11
+ # @param [String] id Query ID
12
+ # @param [Hash] headers Request headers
13
+ #
14
+ # @return [Ksql::Connection::Request] Request instance
15
+ #
16
+ def self.build(id, headers:)
17
+ ::Ksql::Connection::Request.new(
18
+ { queryId: id },
19
+ '/close-query',
20
+ Headers.merge(headers),
21
+ :post
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ksql
4
+ module Api
5
+ class Ksql
6
+ Headers = { 'Accept' => 'application/json' }.freeze
7
+
8
+ #
9
+ # Build the ksqlDB /ksql request
10
+ #
11
+ # @param [String] ksql SQL Statement
12
+ # @param [Integer] command_sequence_number The statements will not be run until all existing commands have completed.
13
+ # @param [Hash] headers Request headers
14
+ # @param [Hash] session_variables Variable substitution values
15
+ # @param [Hash] streams_properties Property overrides to run the statements with
16
+ #
17
+ # @return [Ksql::Connection::Request] Request instance
18
+ #
19
+ def self.build(ksql, command_sequence_number:, headers:, session_variables:, streams_properties:)
20
+ ::Ksql::Connection::Request.new(
21
+ {
22
+ ksql: ksql,
23
+ commandSequenceNumber: command_sequence_number,
24
+ sessionVariables: session_variables,
25
+ streamsProperties: streams_properties,
26
+ }.compact,
27
+ '/ksql',
28
+ Headers.merge(headers),
29
+ :post
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ksql
4
+ module Api
5
+ class Query
6
+ Headers = { 'Accept' => 'application/json' }.freeze
7
+
8
+ #
9
+ # Build the ksqlDB /query-stream request
10
+ #
11
+ # @param [String] sql SQL Statement
12
+ # @param [Hash] headers Request headers
13
+ # @param [Hash] properties Optional properties for the query
14
+ # @param [Hash] session_variables Variable substitution values
15
+ #
16
+ # @return [Ksql::Connection::Request] Request instance
17
+ #
18
+ def self.build(sql, headers:, properties:, session_variables:)
19
+ ::Ksql::Connection::Request.new(
20
+ {
21
+ sql: sql,
22
+ properties: properties,
23
+ sessionVariables: session_variables
24
+ }.compact,
25
+ '/query-stream',
26
+ self::Headers.merge(headers),
27
+ :post
28
+ )
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ksql
4
+ module Api
5
+ class Stream < Query
6
+ Headers = { 'Accept' => 'application/vnd.ksqlapi.delimited.v1' }.freeze
7
+
8
+ # Inherits from Ksql::Api::Query overriding the request Accept header
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ksql
4
+ class Client
5
+ class << self
6
+ #
7
+ # Request /close-query endpoint
8
+ #
9
+ # @param [String] id Query ID
10
+ # @param [Hash] headers Request headers
11
+ #
12
+ # @return [Array/Hash/String/Integer] Request result
13
+ #
14
+ def close_query(id, headers: {})
15
+ request = Api::CloseQuery.build(id, headers: headers)
16
+ result = Connection::Client.call_sync(request)
17
+ Handlers::Raw.handle(result)
18
+ end
19
+
20
+ #
21
+ # Request /ksql endpoint
22
+ #
23
+ # @param [String] ksql SQL Statement
24
+ # @param [Integer] command_sequence_number The statements will not be run until all existing commands have completed.
25
+ # @param [Hash] headers Request headers
26
+ # @param [Hash] session_variables Variable substitution values
27
+ # @param [Hash] streams_properties Property overrides to run the statements with
28
+ #
29
+ # @return [Ksql::@type] Request result
30
+ #
31
+ def ksql(ksql, command_sequence_number: nil, headers: {}, session_variables: {}, streams_properties: {})
32
+ request = Api::Ksql.build(ksql, command_sequence_number: command_sequence_number, headers: headers, session_variables: session_variables, streams_properties: streams_properties)
33
+ result = Connection::Client.call_sync(request)
34
+ Handlers::TypedRow.handle(result)
35
+ end
36
+
37
+ #
38
+ # Request /query-stream endpoint synchronously
39
+ #
40
+ # @param [String] sql SQL Statement
41
+ # @param [Hash] headers Request headers
42
+ # @param [Hash] properties Optional properties for the query
43
+ # @param [Hash] session_variables Variable substitution values
44
+ #
45
+ # @return [Ksql::Connection::Request] Request result
46
+ #
47
+ def query(sql, headers: {}, properties: {}, session_variables: {})
48
+ request = Api::Query.build(sql, headers: headers, properties: properties, session_variables: session_variables)
49
+ result = Connection::Client.call_sync(request)
50
+ Handlers::Collection.handle(result)
51
+ end
52
+
53
+ #
54
+ # Request /query-stream endpoint asynchronously
55
+ #
56
+ # @param [String] sql SQL Statement
57
+ # @param [Hash] headers Request headers
58
+ # @param [Hash] properties Optional properties for the query
59
+ # @param [Hash] session_variables Variable substitution values
60
+ #
61
+ # @return [Ksql::Stream] Stream instance
62
+ #
63
+ def stream(sql, headers: {}, properties: {}, session_variables: {})
64
+ request = Api::Stream.build(sql, headers: headers, properties: properties, session_variables: session_variables)
65
+ client, prepared_request = Connection::Client.call_async(request)
66
+ Handlers::Stream.handle(client, prepared_request)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Ksql
6
+ class Collection < Struct.new(:rows)
7
+ include Enumerable
8
+
9
+ #
10
+ # * Generate a class to fit ksqlDB query returned data
11
+ # * Populate the collection
12
+ #
13
+ # @param [Hash] struct_schema Struct Schema definition
14
+ # @param [Array] items Collection rows
15
+ #
16
+ def initialize(struct_schema, items)
17
+ struct = Ksql.const_set(id_to_struct(struct_schema['queryId']), Class.new(Struct.new(*struct_schema['columnNames'].map { |n| n.downcase.to_sym })))
18
+ self.rows = items.map { |i| struct.new(*i) }
19
+ end
20
+
21
+ #
22
+ # Allow iterations block on Rows
23
+ #
24
+ # @param [Block] &block Block to execute on each row
25
+ #
26
+ # @return [Array] Rows enumerable
27
+ #
28
+ def each(&block)
29
+ rows.each do |r|
30
+ yield(r)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ #
37
+ # Dynamically generate a name based on the ksqlDB Query ID
38
+ #
39
+ # @param [String] id ksqlDB Query ID
40
+ #
41
+ # @return [String] Collection element Struct name
42
+ #
43
+ def id_to_struct(id)
44
+ struct_id = id.present? ? id.strip.gsub('-', '_') : SecureRandom.hex
45
+ "Query#{struct_id}Row"
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Ksql::Configuration < OpenStruct; end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net-http2'
4
+
5
+ module Ksql
6
+ module Connection
7
+ class Client
8
+ class << self
9
+ #
10
+ # Execute the HTTP2 Sync Request
11
+ #
12
+ # @param [Ksql::Connection::Request] request Built request
13
+ #
14
+ # @return [Ksql::Connection::Response] HTTP2 Request response
15
+ #
16
+ def call_sync(request)
17
+ response = client.call(*request.to_params)
18
+ ::Ksql::Connection::Response.new(body: response.body, headers: response.headers)
19
+ end
20
+
21
+ #
22
+ # Prepare the HTTP2 Async Request based on the built input request
23
+ #
24
+ # @param [Ksql::Connection::Request] request Built request
25
+ #
26
+ # @return [Array] Client, Built Async Request
27
+ #
28
+ def call_async(request)
29
+ @@client = client
30
+ prepared_request = @@client.prepare_request(*request.to_params)
31
+ return @@client, prepared_request
32
+ end
33
+
34
+ private
35
+
36
+ #
37
+ # Return HTTP2 Client instance
38
+ #
39
+ # @return [NetHttp2::Client] HTTP2 Client
40
+ #
41
+ def client
42
+ NetHttp2::Client.new(::Ksql.config.host)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ksql
4
+ module Connection
5
+ class Request < Struct.new(:body, :endpoint, :headers, :method)
6
+ #
7
+ # Returns the request params
8
+ #
9
+ # @return [Array] Request params
10
+ #
11
+ def to_params
12
+ return method, endpoint, { body: body.to_json, headers: headers.merge(auth_headers) }
13
+ end
14
+
15
+ private
16
+
17
+ #
18
+ # Prepares Authorization headers if Auth is configured
19
+ #
20
+ # @return [Hash] Authorization headers
21
+ #
22
+ def auth_headers
23
+ ::Ksql.config.auth.present? ? { 'Authorization' => "Basic #{::Ksql.config.auth}" } : {}
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ksql
4
+ module Connection
5
+ class Response
6
+ attr_reader :body, :headers
7
+
8
+ STATUS_KEY = ':status'.freeze
9
+
10
+ def initialize(body:, headers:)
11
+ @body = JSON.parse(body)
12
+ @headers = headers
13
+ end
14
+
15
+ #
16
+ # Check whether or not a Request has returned an Error
17
+ #
18
+ # @return [Boolean] True if error
19
+ #
20
+ def error?
21
+ !headers[STATUS_KEY].to_s.match?(/20[01]/)
22
+ end
23
+ end
24
+ end
25
+ end
data/lib/ksql/error.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ksql
4
+ class Error < OpenStruct
5
+ # Description here
6
+ end
7
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ksql
4
+ module Handlers
5
+ class Collection
6
+ #
7
+ # Handle the response to generate an Enumerable collection
8
+ #
9
+ # @param [Ksql::Connection::Response] response HTTP2 Request response
10
+ #
11
+ # @return [Ksql::Collection] Collection result
12
+ #
13
+ def self.handle(response)
14
+ return Ksql::Error.new(response.body) if response.error?
15
+
16
+ body_dup = response.body
17
+ headers = body_dup.shift
18
+ Ksql::Collection.new(headers, body_dup)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ksql
4
+ module Handlers
5
+ class Raw
6
+ #
7
+ # Return the Response raw parsed body
8
+ #
9
+ # @param [Ksql::Connection::Response] response HTTP2 Request response
10
+ #
11
+ # @return [Array/Hash/String/Integer] Response parsed body
12
+ #
13
+ def self.handle(response)
14
+ return Ksql::Error.new(response.body) if response.error?
15
+
16
+ response.body
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ksql
4
+ module Handlers
5
+ class Stream
6
+ #
7
+ # Instanciate a Ksql::Stream class to handle the streamed connection
8
+ #
9
+ # @param [Ksql::Connection::Client] client Client instance
10
+ # @param [Ksql::Connection::Request] request Built request
11
+ #
12
+ # @return [Ksql::Stream] Stream instance
13
+ #
14
+ def self.handle(client, request)
15
+ ::Ksql::Stream.new(client, request)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ksql
4
+ module Handlers
5
+ class TypedRow
6
+ #
7
+ # Define and instanciate an OpenStruct to fit the typed request response
8
+ #
9
+ # @param [Ksql::Connection::Response] response HTTP2 Request response
10
+ #
11
+ # @return [Ksql::@type] OpenStruct instance
12
+ #
13
+ def self.handle(response)
14
+ return Ksql::Error.new(response.body) if response.error?
15
+ return response.body unless response.body.present?
16
+
17
+ parsed_body = response.body.first
18
+ row_type = parsed_body.delete('@type').camelize
19
+ row_class = Ksql.const_defined?(row_type) ? "Ksql::#{row_type}".constantize : Ksql.const_set(row_type, Class.new(OpenStruct))
20
+ row_class.new(parsed_body)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ksql
4
+ class StreamError < StandardError; end
5
+
6
+ class Stream
7
+ attr_reader :id
8
+
9
+ def initialize(client, request)
10
+ @client = client
11
+ @request = request
12
+ end
13
+
14
+ #
15
+ # Close the streaming connection
16
+ #
17
+ def close
18
+ raise StreamError.new('The stream hasn\'t stared!') unless @id.present?
19
+
20
+ @client.close
21
+ end
22
+
23
+ #
24
+ # Specify the action to take when the Streaming connection gets closed.
25
+ #
26
+ # @param [Block] &block Code to execute on connection closure
27
+ #
28
+ def on_close(&block)
29
+ @client.on(:close) { yield }
30
+ end
31
+
32
+ #
33
+ # Specify the action to take when the Streaming connection raises an error.
34
+ #
35
+ # @param [Block] &block Code to execute when connection errors occur
36
+ #
37
+ def on_error(&block)
38
+ @client.on(:error) { |e| yield(e) }
39
+ end
40
+
41
+ #
42
+ # Streaming connection handler
43
+ #
44
+ # * Start the stream
45
+ # * Wrap the stream events into OpenStruct instances
46
+ # * Execute the passed block
47
+ #
48
+ # @param [Block] &block Code to execute each time an event arrives
49
+ #
50
+ def start(&block)
51
+ @headers = {}
52
+
53
+ @request.on(:headers) { |headers| @headers = headers }
54
+
55
+ @request.on(:body_chunk) do |body|
56
+ next unless body.present?
57
+
58
+ response = Ksql::Connection::Response.new(body: body, headers: @headers)
59
+ raise Ksql::StreamError.new(response.body['message']) if response.error?
60
+
61
+ if response.body.is_a? Hash
62
+ @event_class = build_event_class(response.body)
63
+ next
64
+ else
65
+ event = @event_class.new(*response.body)
66
+ end
67
+
68
+ yield(event)
69
+ end
70
+
71
+ @client.call_async(@request)
72
+ end
73
+
74
+ private
75
+
76
+ #
77
+ # Define a Struct class to fit the streaming data into.
78
+ #
79
+ # @param [Hash] schema Query schema
80
+ #
81
+ # @return [Ksql::Row] Stream event class
82
+ #
83
+ def build_event_class(schema)
84
+ @id = schema['queryId']
85
+ row_const = "Stream#{schema['queryId'].gsub('-', '_')}Row"
86
+ Ksql.const_defined?(row_const) ? "Ksql::#{row_const}".constantize : Ksql.const_set(row_const, Class.new(Struct.new(*schema['columnNames'].map { |c| c.downcase.to_sym })))
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ksql
4
+ VERSION = '0.1.0.beta'
5
+ end
data/lib/ksql.rb ADDED
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/core_ext/string'
5
+ require 'json'
6
+ require 'ostruct'
7
+
8
+ require_relative 'ksql/api/close_query'
9
+ require_relative 'ksql/api/ksql'
10
+ require_relative 'ksql/api/query'
11
+ require_relative 'ksql/api/stream'
12
+
13
+ require_relative 'ksql/handlers/collection'
14
+ require_relative 'ksql/handlers/raw'
15
+ require_relative 'ksql/handlers/stream'
16
+ require_relative 'ksql/handlers/typed_row'
17
+
18
+ require_relative 'ksql/connection/client'
19
+ require_relative 'ksql/connection/request'
20
+ require_relative 'ksql/connection/response'
21
+
22
+ require_relative 'ksql/client'
23
+ require_relative 'ksql/collection'
24
+ require_relative 'ksql/configuration'
25
+ require_relative 'ksql/error'
26
+ require_relative 'ksql/stream'
27
+
28
+ require_relative 'ksql/version'
29
+
30
+ module Ksql
31
+ def self.config
32
+ @config ||= Ksql::Configuration.new
33
+ end
34
+
35
+ def self.configure
36
+ yield(config)
37
+ end
38
+ end
data/sig/ksql.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Ksql
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end