virgodb 0.1.0.dev.e373706-x86_64-linux-gnu → 0.1.1-x86_64-linux-gnu
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 +4 -4
- data/lib/virgodb/rows.rb +45 -0
- data/lib/virgodb/table.rb +82 -0
- data/lib/virgodb/time_helpers.rb +45 -0
- data/lib/virgodb/version.rb +1 -1
- data/lib/virgodb/virgodb_ruby.so +0 -0
- data/lib/virgodb.rb +3 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4aad06a98bee583d89ff9cdc8cd0790caaa3d88f8168ebbecaf8808563797c50
|
|
4
|
+
data.tar.gz: 696c058c6f8fda74c10c7ad6bdbc8a9060fa230c430683d2ec7ce10ca243dd9f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2927c20cdfa1f9d5aa21e0dacf33d1db31df24347dd6296604c28d8f611c110944a53bc8bd3be479d7481773fa6a283372d9d5b910da6146b75d228860c16dcd
|
|
7
|
+
data.tar.gz: bbfa4a4bac7c6ad8f0b9b4cf0a6a661df1643e96ebc44c91fd8d9c5cf98366391c5f5be1f0d3ea1e2ef98cd26701162ec3cf4b2cd2293d7b92f53256a1078dcd
|
data/lib/virgodb/rows.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Virgodb
|
|
4
|
+
# Wraps virgodb's JSON rows (Array of string-keyed Hash) in a real Struct
|
|
5
|
+
# so callers can use either dot access (row.current_path) or bracket
|
|
6
|
+
# access (row["total_clicks"]). Struct instead of OpenStruct: shared
|
|
7
|
+
# method dispatch across instances, not one dynamically-defined accessor
|
|
8
|
+
# set per object.
|
|
9
|
+
module Rows
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# The Struct CLASS is memoized by field-name shape, not rebuilt per row
|
|
13
|
+
# or per request -- a real caller only ever produces a small closed set
|
|
14
|
+
# of shapes (one per distinct SELECT alias list), so the cache
|
|
15
|
+
# converges to a couple dozen entries for the app's lifetime. Building
|
|
16
|
+
# a fresh anonymous Struct per row would pollute the method cache and
|
|
17
|
+
# grow the global class table.
|
|
18
|
+
#
|
|
19
|
+
# `fields` MUST be the caller's own known SELECT column list (its
|
|
20
|
+
# outer query's alias names, in order) -- NOT inferred from the actual
|
|
21
|
+
# JSON rows. virgodb's JSON serialization OMITS a key entirely when
|
|
22
|
+
# that row's value for it is NULL (e.g. sumIf(revenue, ...) on a path
|
|
23
|
+
# with zero conversions), rather than emitting `"total_revenue": null`.
|
|
24
|
+
# Deriving the shape from the union of every row's own keys would miss
|
|
25
|
+
# a column that's NULL across every row in a batch (e.g. a
|
|
26
|
+
# page-filtered query narrowed to a path with zero conversions) --
|
|
27
|
+
# only the caller's own SQL genuinely knows the full column list
|
|
28
|
+
# regardless of what any individual result happens to contain.
|
|
29
|
+
#
|
|
30
|
+
# `Struct#new(keyword_init: true)` already defaults any key ABSENT
|
|
31
|
+
# from a given row's hash to nil on its own, so a plain
|
|
32
|
+
# `row[field.to_s]` lookup (not a keys-actually-present transform)
|
|
33
|
+
# handles the all-NULL-column case correctly.
|
|
34
|
+
STRUCT_CACHE = {}
|
|
35
|
+
STRUCT_CACHE_MUTEX = Mutex.new
|
|
36
|
+
private_constant :STRUCT_CACHE, :STRUCT_CACHE_MUTEX
|
|
37
|
+
|
|
38
|
+
def to_structs(rows, fields)
|
|
39
|
+
klass = STRUCT_CACHE[fields] ||
|
|
40
|
+
STRUCT_CACHE_MUTEX.synchronize { STRUCT_CACHE[fields] ||= Struct.new(*fields, keyword_init: true) }
|
|
41
|
+
|
|
42
|
+
rows.map { |row| klass.new(**fields.to_h { |f| [ f, row[f.to_s] ] }) }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Virgodb
|
|
7
|
+
# A memoized Writer + its maintenance thread, both created lazily on first
|
|
8
|
+
# use -- not eagerly at process boot, since Writer.new and
|
|
9
|
+
# Virgodb.start_maintenance both raise if this table hasn't been migrated
|
|
10
|
+
# yet, and eager init would crash the whole process on that instead of
|
|
11
|
+
# just failing the first write attempt. Mutex-guarded so concurrent
|
|
12
|
+
# callers in one process can't race to double-initialize.
|
|
13
|
+
#
|
|
14
|
+
# This is the boilerplate every host app embedding virgodb needs around
|
|
15
|
+
# the raw Writer/start_maintenance/query FFI -- written once here instead
|
|
16
|
+
# of once per host app. Deliberately does NOT rescue write failures: a
|
|
17
|
+
# host app's own error-handling policy (swallow-and-log vs. raise, retry,
|
|
18
|
+
# ...) belongs to the host app, not this class -- a caller-side wrapper
|
|
19
|
+
# exposing both a safe and a raising write method is one reasonable
|
|
20
|
+
# shape for that policy.
|
|
21
|
+
class Table
|
|
22
|
+
def initialize(manifest_path:, data_dir:, sql_table_name:, key_column:, sort_by:, partition_column:, tier1_max_rows:, tier1_max_age_secs:, tuning:)
|
|
23
|
+
@manifest_path = manifest_path
|
|
24
|
+
@data_dir = data_dir
|
|
25
|
+
@sql_table_name = sql_table_name
|
|
26
|
+
@key_column = key_column
|
|
27
|
+
@sort_by = sort_by
|
|
28
|
+
@partition_column = partition_column
|
|
29
|
+
@tier1_max_rows = tier1_max_rows
|
|
30
|
+
@tier1_max_age_secs = tier1_max_age_secs
|
|
31
|
+
@tuning = tuning
|
|
32
|
+
@mutex = Mutex.new
|
|
33
|
+
@maintenance_started = false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
attr_reader :manifest_path
|
|
37
|
+
|
|
38
|
+
def write_row(**attrs)
|
|
39
|
+
writer.write_row(**attrs)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# `tombstone` alone doesn't touch the Writer -- calling `writer` first
|
|
43
|
+
# ensures this table's maintenance thread (and therefore its physical
|
|
44
|
+
# tombstone-purge path) is actually running before a tombstone gets
|
|
45
|
+
# recorded for it to eventually act on.
|
|
46
|
+
def tombstone(column:, value:)
|
|
47
|
+
writer
|
|
48
|
+
Virgodb.tombstone(@manifest_path, @sql_table_name, column.to_s, value.to_s)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Returns parsed JSON rows (Array of String-keyed Hash). `sql` must
|
|
52
|
+
# refer to the table by its real SQL name (the `sql_table_name` this
|
|
53
|
+
# instance was built with), not any shorthand a caller's own registry
|
|
54
|
+
# might use for it.
|
|
55
|
+
def query(sql, partition_keys: nil)
|
|
56
|
+
JSON.parse(Virgodb.query(@manifest_path, @sql_table_name, sql, partition_keys))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def flush!
|
|
60
|
+
writer.flush!
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def writer
|
|
66
|
+
@writer || @mutex.synchronize { @writer ||= build_writer }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def build_writer
|
|
70
|
+
ensure_maintenance_started
|
|
71
|
+
Virgodb::Writer.new(@manifest_path, @sql_table_name, @tier1_max_rows, @tier1_max_age_secs)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def ensure_maintenance_started
|
|
75
|
+
return if @maintenance_started
|
|
76
|
+
|
|
77
|
+
FileUtils.mkdir_p(@data_dir)
|
|
78
|
+
Virgodb.start_maintenance(@manifest_path, @sql_table_name, @data_dir, @key_column, @sort_by, @tuning, @partition_column)
|
|
79
|
+
@maintenance_started = true
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require "time"
|
|
2
|
+
|
|
3
|
+
module Virgodb
|
|
4
|
+
# Helpers matching virgodb's own wire format, not any host app's business
|
|
5
|
+
# logic -- every embedding app needs the exact same code, so it lives here
|
|
6
|
+
# once instead of being independently reimplemented (and independently
|
|
7
|
+
# mis-implemented) per host app.
|
|
8
|
+
module TimeHelpers
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# `Virgodb.query` serializes Timestamp columns as zone-less ISO 8601
|
|
12
|
+
# strings (arrow-json, not raw epoch integers) -- any caller reading one
|
|
13
|
+
# back and needing epoch seconds again MUST parse it as UTC explicitly.
|
|
14
|
+
# A bare `Time.parse` interprets a zone-less string in the system's
|
|
15
|
+
# LOCAL timezone: on a non-UTC dev box, that silently shifts a written
|
|
16
|
+
# epoch by the local UTC offset on read-back (a real bug, caught by a
|
|
17
|
+
# host app running this on a non-UTC dev box before it shipped).
|
|
18
|
+
# Appending "Z" is what forces UTC interpretation -- verified directly
|
|
19
|
+
# under a non-UTC TZ before this shipped, same discipline as the bug
|
|
20
|
+
# this replaces. No ActiveSupport dependency (this gem doesn't require
|
|
21
|
+
# Rails): `Time.find_zone` isn't plain Ruby, this stdlib `time`-library
|
|
22
|
+
# trick is.
|
|
23
|
+
def parse_time(value)
|
|
24
|
+
return nil if value.nil? || value.empty?
|
|
25
|
+
Time.parse("#{value}Z").to_i
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# ClickHouse's `toYYYYMM(time)`-equivalent: the month bucket (e.g.
|
|
29
|
+
# "202607") `time` falls in, matching exactly what
|
|
30
|
+
# crates/ingest::month_partition_key_from_timestamp writes into
|
|
31
|
+
# run_files.partition_key -- pure local computation, no DB round trip.
|
|
32
|
+
def month_partition_key(time)
|
|
33
|
+
time.strftime("%Y%m")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# A real time RANGE (e.g. a narrow lookup window) can straddle a month
|
|
37
|
+
# boundary even though any single instant can't -- every distinct month
|
|
38
|
+
# bucket `range` touches, so a range-bounded query can pass this
|
|
39
|
+
# straight to a table's partition-key pruning without silently missing
|
|
40
|
+
# a row that landed just across the boundary.
|
|
41
|
+
def month_partition_keys_for(range)
|
|
42
|
+
[ month_partition_key(range.begin), month_partition_key(range.end) ].uniq
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/virgodb/version.rb
CHANGED
data/lib/virgodb/virgodb_ruby.so
CHANGED
|
Binary file
|
data/lib/virgodb.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: virgodb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: x86_64-linux-gnu
|
|
6
6
|
authors:
|
|
7
7
|
- virgodb
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: |
|
|
14
14
|
Embeds virgodb (an OLAP engine backed by Parquet + DataFusion) directly into a Ruby process
|
|
@@ -21,6 +21,9 @@ extra_rdoc_files: []
|
|
|
21
21
|
files:
|
|
22
22
|
- LICENSE
|
|
23
23
|
- lib/virgodb.rb
|
|
24
|
+
- lib/virgodb/rows.rb
|
|
25
|
+
- lib/virgodb/table.rb
|
|
26
|
+
- lib/virgodb/time_helpers.rb
|
|
24
27
|
- lib/virgodb/version.rb
|
|
25
28
|
- lib/virgodb/virgodb_ruby.so
|
|
26
29
|
homepage:
|