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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a75e70c710d3b2e1111368b0297640676376092f19e98fd6d69500bc5b715467
4
- data.tar.gz: c71c7fdcba8387ccbbb0822cf3ba942cd2c8c4424c495fd1ee45932743342f06
3
+ metadata.gz: 4aad06a98bee583d89ff9cdc8cd0790caaa3d88f8168ebbecaf8808563797c50
4
+ data.tar.gz: 696c058c6f8fda74c10c7ad6bdbc8a9060fa230c430683d2ec7ce10ca243dd9f
5
5
  SHA512:
6
- metadata.gz: 219e45185206a02d2b2720684de63b2a182cd1084090ad0a372c817c2b2a881c776c45c8d129edfcde503f071df096283c64856d08b7b7921dd0fae837d5d53f
7
- data.tar.gz: eb881c951a994a503c3caa6a39a61dfea5bde6095f5313ef45f05b8a1a114236e307e50819a6e461db28b7c97ef97ea13fa2c222172c7d16c1ff924a5a61a889
6
+ metadata.gz: 2927c20cdfa1f9d5aa21e0dacf33d1db31df24347dd6296604c28d8f611c110944a53bc8bd3be479d7481773fa6a283372d9d5b910da6146b75d228860c16dcd
7
+ data.tar.gz: bbfa4a4bac7c6ad8f0b9b4cf0a6a661df1643e96ebc44c91fd8d9c5cf98366391c5f5be1f0d3ea1e2ef98cd26701162ec3cf4b2cd2293d7b92f53256a1078dcd
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Virgodb
4
- VERSION = "0.1.0.dev.e373706"
4
+ VERSION = "0.1.1"
5
5
  end
Binary file
data/lib/virgodb.rb CHANGED
@@ -2,3 +2,6 @@
2
2
 
3
3
  require "virgodb/version"
4
4
  require "virgodb/virgodb_ruby"
5
+ require "virgodb/time_helpers"
6
+ require "virgodb/rows"
7
+ require "virgodb/table"
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.0.dev.e373706
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-09 00:00:00.000000000 Z
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: