kweerie 0.1.0 → 0.1.2
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/README.md +126 -0
- data/lib/generators/kweerie/kweerie_generator.rb +3 -3
- data/lib/kweerie/base_objects.rb +202 -0
- data/lib/kweerie/version.rb +1 -1
- data/lib/kweerie.rb +1 -0
- metadata +8 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f11c69ed8356612d3a1df5dfb98b4751061db706908f7b2efe6ace375f53c73e
|
4
|
+
data.tar.gz: b33644c5b64bb941bf88bf6207ced00706e84a1c83c2b60fa9c7dddc680dd0fe
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9f05d4db4050c840ac87c60b60e44dd27413fc3fee685cd81fd5320d6bb9d37f01ee5a1f29691989f1ec4f3de2a566d1a8fd0057696b54982bdd012170a0ea2b
|
7
|
+
data.tar.gz: 11560ae4acff559585472f020d29d0367ca431cd3f9398cfb75aa1e27b6a08057e8f6b1d5270b7f6ef043551c9f15d3b95434d586400390c512c75b410dc7661
|
data/README.md
CHANGED
@@ -53,6 +53,132 @@ results = UserSearch.with(
|
|
53
53
|
# => [{"id"=>9981, "name"=>"John Doe", "email"=>"johndoe@example.com"}]
|
54
54
|
```
|
55
55
|
|
56
|
+
### Object Mapping
|
57
|
+
|
58
|
+
While `Kweerie::Base` returns plain hashes, you can use `Kweerie::BaseObjects` to get typed Ruby objects with proper attribute methods:
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
class UserSearch < Kweerie::BaseObjects
|
62
|
+
bind :name, as: '$1'
|
63
|
+
bind :created_at, as: '$2'
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns array of objects instead of hashes
|
67
|
+
users = UserSearch.with(
|
68
|
+
name: 'Claude',
|
69
|
+
created_at: '2024-01-01'
|
70
|
+
)
|
71
|
+
|
72
|
+
user = users.first
|
73
|
+
user.name # => "Claude"
|
74
|
+
user.created_at # => 2024-01-01 00:00:00 +0000 (Time object)
|
75
|
+
```
|
76
|
+
|
77
|
+
### Automatic Type Casting
|
78
|
+
|
79
|
+
BaseObjects automatically casts common PostgreSQL types to their Ruby equivalents:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
# In your SQL file
|
83
|
+
SELECT
|
84
|
+
name,
|
85
|
+
created_at, -- timestamp
|
86
|
+
age::integer, -- integer
|
87
|
+
score::float, -- float
|
88
|
+
active::boolean, -- boolean
|
89
|
+
metadata::jsonb, -- jsonb
|
90
|
+
tags::jsonb -- jsonb array
|
91
|
+
FROM users;
|
92
|
+
|
93
|
+
# In your Ruby code
|
94
|
+
user = UserSearch.with(name: 'Claude').first
|
95
|
+
|
96
|
+
user.created_at # => Time object
|
97
|
+
user.age # => Integer
|
98
|
+
user.score # => Float
|
99
|
+
user.active # => true/false
|
100
|
+
user.metadata # => Hash with string keys
|
101
|
+
user.tags # => Array
|
102
|
+
|
103
|
+
# Nested JSONB data is properly accessible
|
104
|
+
user.metadata["role"] # => "admin"
|
105
|
+
user.metadata["preferences"]["theme"] # => "dark"
|
106
|
+
```
|
107
|
+
|
108
|
+
### Pattern Matching Support
|
109
|
+
|
110
|
+
BaseObjects support Ruby's pattern matching syntax:
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
case user
|
114
|
+
in { name:, metadata: { role: "admin" } }
|
115
|
+
puts "Admin user: #{name}"
|
116
|
+
in { name:, metadata: { role: "user" } }
|
117
|
+
puts "Regular user: #{name}"
|
118
|
+
end
|
119
|
+
|
120
|
+
# Nested pattern matching
|
121
|
+
case user
|
122
|
+
in { metadata: { preferences: { theme: "dark" } } }
|
123
|
+
puts "Dark theme user"
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
### Object Interface
|
128
|
+
|
129
|
+
BaseObjects provide several useful methods:
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
# Hash-like access
|
133
|
+
user[:name] # => "Claude"
|
134
|
+
user.fetch(:email, 'N/A') # => Returns 'N/A' if email is nil
|
135
|
+
|
136
|
+
# Serialization
|
137
|
+
user.to_h # => Hash with string keys
|
138
|
+
user.to_json # => JSON string
|
139
|
+
|
140
|
+
# Comparison
|
141
|
+
user1 == user2 # Compare all attributes
|
142
|
+
users.sort_by(&:created_at) # Sortable
|
143
|
+
|
144
|
+
# Change tracking
|
145
|
+
user.changed? # => Check if any attributes changed
|
146
|
+
user.changes # => Hash of changes with [old, new] values
|
147
|
+
user.original_attributes # => Original attributes from DB
|
148
|
+
```
|
149
|
+
|
150
|
+
## PostgreSQL Array Support
|
151
|
+
|
152
|
+
BaseObjects handles PostgreSQL arrays by converting them to Ruby arrays with proper type casting:
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
# In your PostgreSQL schema
|
156
|
+
create_table :users do |t|
|
157
|
+
t.integer :preferred_ordering, array: true, default: []
|
158
|
+
t.string :tags, array: true
|
159
|
+
t.float :scores, array: true
|
160
|
+
end
|
161
|
+
|
162
|
+
# In your query
|
163
|
+
user = UserSearch.with(name: 'Claude').first
|
164
|
+
|
165
|
+
user.preferred_ordering # => [1, 3, 2]
|
166
|
+
user.tags # => ["ruby", "rails"]
|
167
|
+
user.scores # => [98.5, 87.2, 92.0]
|
168
|
+
```
|
169
|
+
|
170
|
+
## Performance Considerations
|
171
|
+
|
172
|
+
BaseObjects creates a unique class for each query result set, with the following optimizations:
|
173
|
+
|
174
|
+
- Classes are cached and reused for subsequent queries
|
175
|
+
- Attribute readers are defined upfront
|
176
|
+
- Type casting happens once during initialization
|
177
|
+
- No method_missing or dynamic method definition per instance
|
178
|
+
- Efficient pattern matching support
|
179
|
+
|
180
|
+
For queries where you don't need the object interface, use `Kweerie::Base` instead for slightly better performance.
|
181
|
+
|
56
182
|
### Rails Generator
|
57
183
|
|
58
184
|
If you're using Rails, you can use the generator to create new query files:
|
@@ -32,11 +32,11 @@ class KweerieGenerator < Rails::Generators::NamedBase
|
|
32
32
|
-- Write your SQL query here
|
33
33
|
-- Available parameters: #{parameters.map { |p| "$#{parameters.index(p) + 1} (#{p})" }.join(", ")}
|
34
34
|
|
35
|
-
SELECT
|
35
|
+
-- SELECT
|
36
36
|
-- your columns here
|
37
|
-
FROM
|
37
|
+
-- FROM
|
38
38
|
-- your tables here
|
39
|
-
WHERE
|
39
|
+
-- WHERE
|
40
40
|
-- your conditions here
|
41
41
|
SQL
|
42
42
|
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/string"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
module Kweerie
|
7
|
+
class BaseObjects < Base
|
8
|
+
class << self
|
9
|
+
def with(params = {})
|
10
|
+
results = super
|
11
|
+
return [] if results.empty?
|
12
|
+
|
13
|
+
# Create a unique result class for this query
|
14
|
+
result_class = generate_result_class(results.first.keys)
|
15
|
+
|
16
|
+
# Map results to objects
|
17
|
+
results.map { |row| result_class.new(row) }
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def generate_result_class(attribute_names)
|
23
|
+
@generate_result_class ||= Class.new do
|
24
|
+
# Include comparison and serialization modules
|
25
|
+
include Comparable
|
26
|
+
|
27
|
+
# Define attr_readers for all columns
|
28
|
+
attribute_names.each do |name|
|
29
|
+
attr_reader name
|
30
|
+
end
|
31
|
+
|
32
|
+
define_method :type_cast_value do |value|
|
33
|
+
case value
|
34
|
+
when /^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$/ # DateTime check
|
35
|
+
Time.parse(value)
|
36
|
+
when /^\d+$/ # Integer check
|
37
|
+
value.to_i
|
38
|
+
when /^\d*\.\d+$/ # Float check
|
39
|
+
value.to_f
|
40
|
+
when /^(true|false)$/i # Boolean check
|
41
|
+
value.downcase == "true"
|
42
|
+
when /^{.*}$/ # Could be PG array or JSON
|
43
|
+
if value.start_with?("{") && value.end_with?("}") && !value.include?('"=>') && !value.include?(": ")
|
44
|
+
# PostgreSQL array (simple heuristic: no "=>" or ":" suggests it's not JSON)
|
45
|
+
parse_pg_array(value)
|
46
|
+
else
|
47
|
+
# Attempt JSON parse
|
48
|
+
begin
|
49
|
+
parsed = JSON.parse(value)
|
50
|
+
deep_stringify_keys(parsed)
|
51
|
+
rescue JSON::ParserError
|
52
|
+
value
|
53
|
+
end
|
54
|
+
end
|
55
|
+
when /^[\[{]/ # Pure JSON (arrays starting with [ or other JSON objects)
|
56
|
+
begin
|
57
|
+
parsed = JSON.parse(value)
|
58
|
+
deep_stringify_keys(parsed)
|
59
|
+
rescue JSON::ParserError
|
60
|
+
value
|
61
|
+
end
|
62
|
+
else
|
63
|
+
value
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
define_method :initialize do |attrs|
|
68
|
+
# Store original attributes with the same type casting
|
69
|
+
@_original_attributes = attrs.transform_keys(&:to_s).transform_values do |value|
|
70
|
+
type_cast_value(value)
|
71
|
+
end
|
72
|
+
|
73
|
+
attrs.each do |name, value|
|
74
|
+
casted_value = type_cast_value(value)
|
75
|
+
instance_variable_set("@#{name}", casted_value)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
define_method :parse_pg_array do |value|
|
79
|
+
# Remove the curly braces
|
80
|
+
clean_value = value.gsub(/^{|}$/, "")
|
81
|
+
return [] if clean_value.empty?
|
82
|
+
|
83
|
+
# Split on comma, but not within quoted strings
|
84
|
+
elements = clean_value.split(/,(?=(?:[^"]*"[^"]*")*[^"]*$)/)
|
85
|
+
|
86
|
+
elements.map do |element|
|
87
|
+
case element
|
88
|
+
when /^\d+$/ # Integer
|
89
|
+
element.to_i
|
90
|
+
when /^\d*\.\d+$/ # Float
|
91
|
+
element.to_f
|
92
|
+
when /^(true|false)$/i # Boolean
|
93
|
+
element.downcase == "true"
|
94
|
+
when /^"(.*)"$/ # Quoted string
|
95
|
+
::Regexp.last_match(1)
|
96
|
+
else
|
97
|
+
element
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
define_method :deep_symbolize_keys do |obj|
|
103
|
+
case obj
|
104
|
+
when Hash
|
105
|
+
obj.transform_keys(&:to_sym).transform_values { |v| deep_symbolize_keys(v) }
|
106
|
+
when Array
|
107
|
+
obj.map { |item| item.is_a?(Hash) ? deep_symbolize_keys(item) : item }
|
108
|
+
else
|
109
|
+
obj
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Nice inspect output
|
114
|
+
define_method :inspect do
|
115
|
+
attrs = attribute_names.map do |name|
|
116
|
+
"#{name}=#{instance_variable_get("@#{name}").inspect}"
|
117
|
+
end.join(" ")
|
118
|
+
"#<#{self.class.name || "Record"} #{attrs}>"
|
119
|
+
end
|
120
|
+
|
121
|
+
# Hash-like access
|
122
|
+
define_method :[] do |key|
|
123
|
+
instance_variable_get("@#{key}")
|
124
|
+
end
|
125
|
+
|
126
|
+
define_method :fetch do |key, default = nil|
|
127
|
+
instance_variable_defined?("@#{key}") ? instance_variable_get("@#{key}") : default
|
128
|
+
end
|
129
|
+
|
130
|
+
# Comparison methods
|
131
|
+
define_method :<=> do |other|
|
132
|
+
return nil unless other.is_a?(self.class)
|
133
|
+
|
134
|
+
to_h <=> other.to_h
|
135
|
+
end
|
136
|
+
|
137
|
+
define_method :== do |other|
|
138
|
+
return false unless other.is_a?(self.class)
|
139
|
+
|
140
|
+
to_h == other.to_h
|
141
|
+
end
|
142
|
+
|
143
|
+
define_method :eql? do |other|
|
144
|
+
self == other
|
145
|
+
end
|
146
|
+
|
147
|
+
define_method :hash do
|
148
|
+
to_h.hash
|
149
|
+
end
|
150
|
+
|
151
|
+
# Add helper method for deep string keys
|
152
|
+
define_method :deep_stringify_keys do |obj|
|
153
|
+
case obj
|
154
|
+
when Hash
|
155
|
+
obj.transform_keys(&:to_s).transform_values { |v| deep_stringify_keys(v) }
|
156
|
+
when Array
|
157
|
+
obj.map { |item| item.is_a?(Hash) ? deep_stringify_keys(item) : item }
|
158
|
+
else
|
159
|
+
obj
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Serialization
|
164
|
+
define_method :to_h do
|
165
|
+
attribute_names.each_with_object({}) do |name, hash|
|
166
|
+
value = instance_variable_get("@#{name}")
|
167
|
+
# Ensure string keys in output
|
168
|
+
hash[name.to_s] = value.is_a?(Hash) ? deep_stringify_keys(value) : value
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
define_method :to_json do |*args|
|
173
|
+
to_h.to_json(*args)
|
174
|
+
end
|
175
|
+
|
176
|
+
# Pattern matching support (Ruby 2.7+)
|
177
|
+
define_method :deconstruct_keys do |keys|
|
178
|
+
symbolized = deep_symbolize_keys(to_h)
|
179
|
+
keys ? symbolized.slice(*keys) : symbolized
|
180
|
+
end
|
181
|
+
|
182
|
+
# Original attributes access
|
183
|
+
define_method :original_attributes do
|
184
|
+
@_original_attributes
|
185
|
+
end
|
186
|
+
|
187
|
+
# ActiveModel-like changes tracking
|
188
|
+
define_method :changed? do
|
189
|
+
to_h != @_original_attributes
|
190
|
+
end
|
191
|
+
|
192
|
+
define_method :changes do
|
193
|
+
to_h.each_with_object({}) do |(key, value), changes|
|
194
|
+
original = @_original_attributes[key]
|
195
|
+
changes[key] = [original, value] if original != value
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
data/lib/kweerie/version.rb
CHANGED
data/lib/kweerie.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kweerie
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Toby
|
@@ -11,33 +11,33 @@ cert_chain: []
|
|
11
11
|
date: 2024-11-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: minitest
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '5.0'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '5.0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: pg
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '1.0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '1.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rails
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -67,6 +67,7 @@ files:
|
|
67
67
|
- lib/generators/kweerie/kweerie_generator.rb
|
68
68
|
- lib/kweerie.rb
|
69
69
|
- lib/kweerie/base.rb
|
70
|
+
- lib/kweerie/base_objects.rb
|
70
71
|
- lib/kweerie/configuration.rb
|
71
72
|
- lib/kweerie/version.rb
|
72
73
|
- sig/kweerie.rbs
|