mudis-ql 0.1.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 +7 -0
- data/README.md +596 -0
- data/lib/mudis-ql/metrics_scope.rb +175 -0
- data/lib/mudis-ql/scope.rb +184 -0
- data/lib/mudis-ql/store.rb +79 -0
- data/lib/mudis-ql/version.rb +5 -0
- data/lib/mudis-ql.rb +49 -0
- data/spec/mudis-ql/error_handling_spec.rb +330 -0
- data/spec/mudis-ql/integration_spec.rb +337 -0
- data/spec/mudis-ql/metrics_scope_spec.rb +332 -0
- data/spec/mudis-ql/performance_spec.rb +295 -0
- data/spec/mudis-ql/scope_spec.rb +169 -0
- data/spec/mudis-ql/store_spec.rb +77 -0
- data/spec/mudis-ql_spec.rb +52 -0
- metadata +118 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe MudisQL::Scope do
|
|
4
|
+
let(:namespace) { "test_products" }
|
|
5
|
+
let(:store) { MudisQL::Store.new(namespace) }
|
|
6
|
+
let(:scope) { described_class.new(store) }
|
|
7
|
+
|
|
8
|
+
before do
|
|
9
|
+
Mudis.serializer = JSON
|
|
10
|
+
|
|
11
|
+
# Seed test data
|
|
12
|
+
store.write("prod1", { name: "Laptop", price: 1200, category: "electronics", stock: 5 })
|
|
13
|
+
store.write("prod2", { name: "Mouse", price: 25, category: "electronics", stock: 50 })
|
|
14
|
+
store.write("prod3", { name: "Desk", price: 300, category: "furniture", stock: 10 })
|
|
15
|
+
store.write("prod4", { name: "Chair", price: 150, category: "furniture", stock: 20 })
|
|
16
|
+
store.write("prod5", { name: "Keyboard", price: 75, category: "electronics", stock: 30 })
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe "#where" do
|
|
20
|
+
it "filters by exact match" do
|
|
21
|
+
results = scope.where(category: "electronics").all
|
|
22
|
+
expect(results.size).to eq(3)
|
|
23
|
+
expect(results.map { |r| r["name"] }).to contain_exactly("Laptop", "Mouse", "Keyboard")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "filters with proc" do
|
|
27
|
+
results = scope.where(price: ->(p) { p > 100 }).all
|
|
28
|
+
expect(results.size).to eq(3)
|
|
29
|
+
expect(results.map { |r| r["name"] }).to contain_exactly("Laptop", "Desk", "Chair")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "filters with regex" do
|
|
33
|
+
results = scope.where(name: /^[KM]/).all
|
|
34
|
+
expect(results.size).to eq(2)
|
|
35
|
+
expect(results.map { |r| r["name"] }).to contain_exactly("Mouse", "Keyboard")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "filters with range" do
|
|
39
|
+
results = scope.where(price: 50..200).all
|
|
40
|
+
expect(results.size).to eq(2)
|
|
41
|
+
expect(results.map { |r| r["name"] }).to contain_exactly("Chair", "Keyboard")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "chains multiple conditions" do
|
|
45
|
+
results = scope
|
|
46
|
+
.where(category: "electronics")
|
|
47
|
+
.where(price: ->(p) { p < 100 })
|
|
48
|
+
.all
|
|
49
|
+
|
|
50
|
+
expect(results.size).to eq(2)
|
|
51
|
+
expect(results.map { |r| r["name"] }).to contain_exactly("Mouse", "Keyboard")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
describe "#order" do
|
|
56
|
+
it "orders ascending by default" do
|
|
57
|
+
results = scope.order(:price).all
|
|
58
|
+
prices = results.map { |r| r["price"] }
|
|
59
|
+
expect(prices).to eq([25, 75, 150, 300, 1200])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "orders descending" do
|
|
63
|
+
results = scope.order(:price, :desc).all
|
|
64
|
+
prices = results.map { |r| r["price"] }
|
|
65
|
+
expect(prices).to eq([1200, 300, 150, 75, 25])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "orders by string field" do
|
|
69
|
+
results = scope.order(:name).all
|
|
70
|
+
names = results.map { |r| r["name"] }
|
|
71
|
+
expect(names).to eq(["Chair", "Desk", "Keyboard", "Laptop", "Mouse"])
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
describe "#limit" do
|
|
76
|
+
it "limits the number of results" do
|
|
77
|
+
results = scope.limit(3).all
|
|
78
|
+
expect(results.size).to eq(3)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "works with ordering" do
|
|
82
|
+
results = scope.order(:price).limit(2).all
|
|
83
|
+
expect(results.map { |r| r["name"] }).to eq(["Mouse", "Keyboard"])
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
describe "#offset" do
|
|
88
|
+
it "skips the first N results" do
|
|
89
|
+
results = scope.order(:price).offset(2).all
|
|
90
|
+
expect(results.size).to eq(3)
|
|
91
|
+
expect(results.map { |r| r["name"] }).to eq(["Chair", "Desk", "Laptop"])
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "works with limit for pagination" do
|
|
95
|
+
page1 = scope.order(:name).limit(2).offset(0).all
|
|
96
|
+
page2 = scope.order(:name).limit(2).offset(2).all
|
|
97
|
+
|
|
98
|
+
expect(page1.map { |r| r["name"] }).to eq(["Chair", "Desk"])
|
|
99
|
+
expect(page2.map { |r| r["name"] }).to eq(["Keyboard", "Laptop"])
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
describe "#first" do
|
|
104
|
+
it "returns the first matching result" do
|
|
105
|
+
result = scope.where(category: "furniture").order(:price).first
|
|
106
|
+
expect(result["name"]).to eq("Chair")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "returns nil when no results" do
|
|
110
|
+
result = scope.where(category: "nonexistent").first
|
|
111
|
+
expect(result).to be_nil
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
describe "#last" do
|
|
116
|
+
it "returns the last result" do
|
|
117
|
+
result = scope.order(:price).last
|
|
118
|
+
expect(result["name"]).to eq("Laptop")
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
describe "#count" do
|
|
123
|
+
it "counts matching records" do
|
|
124
|
+
count = scope.where(category: "electronics").count
|
|
125
|
+
expect(count).to eq(3)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it "counts all records when no filters" do
|
|
129
|
+
count = scope.count
|
|
130
|
+
expect(count).to eq(5)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
describe "#exists?" do
|
|
135
|
+
it "returns true when records exist" do
|
|
136
|
+
expect(scope.where(category: "electronics").exists?).to be true
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "returns false when no records match" do
|
|
140
|
+
expect(scope.where(category: "nonexistent").exists?).to be false
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
describe "#pluck" do
|
|
145
|
+
it "extracts a single field" do
|
|
146
|
+
names = scope.order(:name).pluck(:name)
|
|
147
|
+
expect(names).to eq(["Chair", "Desk", "Keyboard", "Laptop", "Mouse"])
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it "extracts multiple fields" do
|
|
151
|
+
pairs = scope.where(category: "furniture").pluck(:name, :price)
|
|
152
|
+
expect(pairs).to contain_exactly(["Chair", 150], ["Desk", 300])
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
describe "complex queries" do
|
|
157
|
+
it "combines multiple operations" do
|
|
158
|
+
results = scope
|
|
159
|
+
.where(category: "electronics")
|
|
160
|
+
.where(stock: ->(s) { s >= 25 })
|
|
161
|
+
.order(:price)
|
|
162
|
+
.limit(2)
|
|
163
|
+
.all
|
|
164
|
+
|
|
165
|
+
expect(results.map { |r| r["name"] }).to eq(["Mouse", "Keyboard"])
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe MudisQL::Store do
|
|
4
|
+
let(:namespace) { "test_users" }
|
|
5
|
+
let(:store) { described_class.new(namespace) }
|
|
6
|
+
|
|
7
|
+
before do
|
|
8
|
+
# Ensure mudis is configured
|
|
9
|
+
Mudis.serializer = JSON
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
describe "#initialize" do
|
|
13
|
+
it "sets the namespace" do
|
|
14
|
+
expect(store.namespace).to eq(namespace)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe "#write and #read" do
|
|
19
|
+
it "writes and reads values from mudis" do
|
|
20
|
+
store.write("user1", { name: "Alice", age: 30 })
|
|
21
|
+
result = store.read("user1")
|
|
22
|
+
|
|
23
|
+
expect(result).to eq({ "name" => "Alice", "age" => 30 })
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "supports TTL" do
|
|
27
|
+
store.write("user1", { name: "Bob" }, expires_in: 1)
|
|
28
|
+
expect(store.read("user1")).not_to be_nil
|
|
29
|
+
|
|
30
|
+
sleep 1.1
|
|
31
|
+
expect(store.read("user1")).to be_nil
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
describe "#delete" do
|
|
36
|
+
it "removes a key from the cache" do
|
|
37
|
+
store.write("user1", { name: "Charlie" })
|
|
38
|
+
expect(store.read("user1")).not_to be_nil
|
|
39
|
+
|
|
40
|
+
store.delete("user1")
|
|
41
|
+
expect(store.read("user1")).to be_nil
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe "#keys" do
|
|
46
|
+
it "returns all keys in the namespace" do
|
|
47
|
+
store.write("user1", { name: "Alice" })
|
|
48
|
+
store.write("user2", { name: "Bob" })
|
|
49
|
+
|
|
50
|
+
keys = store.keys
|
|
51
|
+
expect(keys).to contain_exactly("user1", "user2")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
describe "#all" do
|
|
56
|
+
it "returns all records with their keys" do
|
|
57
|
+
store.write("user1", { name: "Alice", age: 30 })
|
|
58
|
+
store.write("user2", { name: "Bob", age: 25 })
|
|
59
|
+
|
|
60
|
+
results = store.all
|
|
61
|
+
expect(results).to contain_exactly(
|
|
62
|
+
{ "name" => "Alice", "age" => 30, "_key" => "user1" },
|
|
63
|
+
{ "name" => "Bob", "age" => 25, "_key" => "user2" }
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "handles non-hash values" do
|
|
68
|
+
store.write("simple", "just a string")
|
|
69
|
+
|
|
70
|
+
results = store.all
|
|
71
|
+
expect(results).to contain_exactly(
|
|
72
|
+
{ "_key" => "simple", "value" => "just a string" }
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe MudisQL do
|
|
4
|
+
it "has a version number" do
|
|
5
|
+
expect(MudisQL::VERSION).not_to be nil
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
describe ".from" do
|
|
9
|
+
it "returns a Scope object" do
|
|
10
|
+
scope = MudisQL.from("test")
|
|
11
|
+
expect(scope).to be_a(MudisQL::Scope)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "supports optional namespace (defaults to nil)" do
|
|
15
|
+
scope = MudisQL.from
|
|
16
|
+
expect(scope).to be_a(MudisQL::Scope)
|
|
17
|
+
expect(scope.instance_variable_get(:@store).namespace).to be_nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "works with keys without explicit namespace" do
|
|
21
|
+
Mudis.write("test:key1", { name: "No namespace" })
|
|
22
|
+
Mudis.write("test:key2", { name: "Also no namespace" })
|
|
23
|
+
|
|
24
|
+
# Note: MudisQL.from with no namespace won't list keys since Mudis.keys requires namespace
|
|
25
|
+
# But individual key access works fine
|
|
26
|
+
scope = MudisQL.from
|
|
27
|
+
expect(scope).to be_a(MudisQL::Scope)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe ".configure" do
|
|
32
|
+
it "yields a configuration object" do
|
|
33
|
+
expect { |b| MudisQL.configure(&b) }.to yield_with_args(MudisQL::Configuration)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "allows setting default_limit" do
|
|
37
|
+
MudisQL.configure do |config|
|
|
38
|
+
config.default_limit = 50
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
expect(MudisQL.configuration.default_limit).to eq(50)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe ".metrics" do
|
|
46
|
+
it "returns a MetricsScope object" do
|
|
47
|
+
metrics = MudisQL.metrics
|
|
48
|
+
expect(metrics).to be_a(MudisQL::MetricsScope)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
metadata
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mudis-ql
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- kiebor81
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2025-12-07 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: mudis
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.9.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.9.0
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: climate_control
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 1.1.0
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 1.1.0
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rspec
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.12'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.12'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: simplecov
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.22'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0.22'
|
|
68
|
+
description: Mudis-QL extends Mudis by providing a SQL-like query interface for data
|
|
69
|
+
stored in the mudis cache
|
|
70
|
+
executables: []
|
|
71
|
+
extensions: []
|
|
72
|
+
extra_rdoc_files: []
|
|
73
|
+
files:
|
|
74
|
+
- README.md
|
|
75
|
+
- lib/mudis-ql.rb
|
|
76
|
+
- lib/mudis-ql/metrics_scope.rb
|
|
77
|
+
- lib/mudis-ql/scope.rb
|
|
78
|
+
- lib/mudis-ql/store.rb
|
|
79
|
+
- lib/mudis-ql/version.rb
|
|
80
|
+
- spec/mudis-ql/error_handling_spec.rb
|
|
81
|
+
- spec/mudis-ql/integration_spec.rb
|
|
82
|
+
- spec/mudis-ql/metrics_scope_spec.rb
|
|
83
|
+
- spec/mudis-ql/performance_spec.rb
|
|
84
|
+
- spec/mudis-ql/scope_spec.rb
|
|
85
|
+
- spec/mudis-ql/store_spec.rb
|
|
86
|
+
- spec/mudis-ql_spec.rb
|
|
87
|
+
homepage: https://github.com/kiebor81/mudis-ql
|
|
88
|
+
licenses:
|
|
89
|
+
- MIT
|
|
90
|
+
metadata:
|
|
91
|
+
homepage_uri: https://github.com/kiebor81/mudis-ql
|
|
92
|
+
source_code_uri: https://github.com/kiebor81/mudis-ql
|
|
93
|
+
changelog_uri: https://github.com/kiebor81/mudis-ql/blob/main/CHANGELOG.md
|
|
94
|
+
rdoc_options: []
|
|
95
|
+
require_paths:
|
|
96
|
+
- lib
|
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - ">="
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '3.0'
|
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
103
|
+
requirements:
|
|
104
|
+
- - ">="
|
|
105
|
+
- !ruby/object:Gem::Version
|
|
106
|
+
version: '0'
|
|
107
|
+
requirements: []
|
|
108
|
+
rubygems_version: 3.6.2
|
|
109
|
+
specification_version: 4
|
|
110
|
+
summary: A simple query DSL for Mudis cache
|
|
111
|
+
test_files:
|
|
112
|
+
- spec/mudis-ql/error_handling_spec.rb
|
|
113
|
+
- spec/mudis-ql/integration_spec.rb
|
|
114
|
+
- spec/mudis-ql/metrics_scope_spec.rb
|
|
115
|
+
- spec/mudis-ql/performance_spec.rb
|
|
116
|
+
- spec/mudis-ql/scope_spec.rb
|
|
117
|
+
- spec/mudis-ql/store_spec.rb
|
|
118
|
+
- spec/mudis-ql_spec.rb
|