querykit 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/CHANGELOG.md +15 -0
- data/LICENSE +21 -0
- data/README.md +179 -0
- data/lib/querykit/adapters/adapter.rb +28 -0
- data/lib/querykit/adapters/mysql_adapter.rb +44 -0
- data/lib/querykit/adapters/postgresql_adapter.rb +50 -0
- data/lib/querykit/adapters/sqlite_adapter.rb +43 -0
- data/lib/querykit/case_builder.rb +102 -0
- data/lib/querykit/configuration.rb +160 -0
- data/lib/querykit/connection.rb +211 -0
- data/lib/querykit/delete_query.rb +54 -0
- data/lib/querykit/extensions/case_when.rb +30 -0
- data/lib/querykit/insert_query.rb +58 -0
- data/lib/querykit/query.rb +473 -0
- data/lib/querykit/repository.rb +182 -0
- data/lib/querykit/update_query.rb +59 -0
- data/lib/querykit/version.rb +5 -0
- data/lib/querykit.rb +110 -0
- data/sig/adapters/adapter.rbs +14 -0
- data/sig/adapters/mysql_adapter.rbs +15 -0
- data/sig/adapters/postgresql_adapter.rbs +15 -0
- data/sig/adapters/sqlite_adapter.rbs +15 -0
- data/sig/case_builder.rbs +23 -0
- data/sig/configuration.rbs +22 -0
- data/sig/connection.rbs +36 -0
- data/sig/delete_query.rbs +22 -0
- data/sig/extensions/case_when.rbs +10 -0
- data/sig/insert_query.rbs +19 -0
- data/sig/query.rbs +83 -0
- data/sig/querykit.rbs +25 -0
- data/sig/repository.rbs +46 -0
- data/sig/update_query.rbs +25 -0
- metadata +120 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QueryKit
|
|
4
|
+
# Connection class to manage database interactions.
|
|
5
|
+
#
|
|
6
|
+
# Provides methods for executing queries, managing transactions,
|
|
7
|
+
# and optionally mapping results to model objects.
|
|
8
|
+
#
|
|
9
|
+
# @example Basic usage
|
|
10
|
+
# db = Quby.connect(:sqlite, database: 'app.db')
|
|
11
|
+
# query = db.query('users').where('age', '>', 18)
|
|
12
|
+
# users = db.get(query)
|
|
13
|
+
#
|
|
14
|
+
# @example With model mapping
|
|
15
|
+
# users = db.get(query, User) # Returns array of User objects
|
|
16
|
+
#
|
|
17
|
+
# @example Transaction
|
|
18
|
+
# db.transaction do
|
|
19
|
+
# db.execute_insert(db.insert('users').values(name: 'John'))
|
|
20
|
+
# db.execute_update(db.update('posts').set(status: 'published'))
|
|
21
|
+
# end
|
|
22
|
+
class Connection
|
|
23
|
+
# @return [Adapter] the database adapter instance
|
|
24
|
+
attr_reader :adapter
|
|
25
|
+
|
|
26
|
+
# Initialize a new Connection with the given adapter.
|
|
27
|
+
#
|
|
28
|
+
# @param adapter [Adapter] a database adapter instance (SQLite, PostgreSQL, or MySQL)
|
|
29
|
+
#
|
|
30
|
+
# @example
|
|
31
|
+
# adapter = Quby::Adapters::SQLiteAdapter.new(database: 'app.db')
|
|
32
|
+
# connection = Quby::Connection.new(adapter)
|
|
33
|
+
def initialize(adapter)
|
|
34
|
+
@adapter = adapter
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Create a new query builder for the specified table.
|
|
38
|
+
#
|
|
39
|
+
# @param table [String, nil] the table name (can be set later with from())
|
|
40
|
+
# @return [Query] a new query builder instance
|
|
41
|
+
#
|
|
42
|
+
# @example
|
|
43
|
+
# query = db.query('users').where('age', '>', 18)
|
|
44
|
+
def query(table = nil)
|
|
45
|
+
Query.new(table)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Create a new query builder for the specified table (alias for query).
|
|
49
|
+
#
|
|
50
|
+
# @param table [String] the table name
|
|
51
|
+
# @return [Query] a new query builder instance
|
|
52
|
+
#
|
|
53
|
+
# @example
|
|
54
|
+
# query = db.from('users').where('status', 'active')
|
|
55
|
+
def from(table)
|
|
56
|
+
Query.new(table)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Create a new query builder for the specified table (alias for query).
|
|
60
|
+
#
|
|
61
|
+
# @param table [String] the table name
|
|
62
|
+
# @return [Query] a new query builder instance
|
|
63
|
+
#
|
|
64
|
+
# @example
|
|
65
|
+
# query = db.table('users').select('*')
|
|
66
|
+
def table(table)
|
|
67
|
+
Query.new(table)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Create a new INSERT query builder.
|
|
71
|
+
#
|
|
72
|
+
# @param table [String, nil] the table name
|
|
73
|
+
# @return [InsertQuery] a new insert query builder instance
|
|
74
|
+
#
|
|
75
|
+
# @example
|
|
76
|
+
# insert = db.insert('users').values(name: 'John', email: 'john@example.com')
|
|
77
|
+
def insert(table = nil)
|
|
78
|
+
InsertQuery.new(table)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Create a new UPDATE query builder.
|
|
82
|
+
#
|
|
83
|
+
# @param table [String, nil] the table name
|
|
84
|
+
# @return [UpdateQuery] a new update query builder instance
|
|
85
|
+
#
|
|
86
|
+
# @example
|
|
87
|
+
# update = db.update('users').set(status: 'inactive').where('last_login', '<', '2020-01-01')
|
|
88
|
+
def update(table = nil)
|
|
89
|
+
UpdateQuery.new(table)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Create a new DELETE query builder.
|
|
93
|
+
#
|
|
94
|
+
# @param table [String, nil] the table name
|
|
95
|
+
# @return [DeleteQuery] a new delete query builder instance
|
|
96
|
+
#
|
|
97
|
+
# @example
|
|
98
|
+
# delete = db.delete('users').where('status', 'deleted')
|
|
99
|
+
def delete(table = nil)
|
|
100
|
+
DeleteQuery.new(table)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Execute a SELECT query and return all results.
|
|
104
|
+
#
|
|
105
|
+
# @param query [Query] the query to execute
|
|
106
|
+
# @param model_class [Class, nil] optional model class to map results to
|
|
107
|
+
#
|
|
108
|
+
# @return [Array<Hash>, Array<Object>] array of result hashes or model instances
|
|
109
|
+
#
|
|
110
|
+
# @example Get raw hashes
|
|
111
|
+
# users = db.get(db.query('users').where('age', '>', 18))
|
|
112
|
+
#
|
|
113
|
+
# @example Map to model objects
|
|
114
|
+
# users = db.get(db.query('users').where('age', '>', 18), User)
|
|
115
|
+
def get(query, model_class = nil)
|
|
116
|
+
sql = query.to_sql
|
|
117
|
+
results = @adapter.execute(sql, query.bindings)
|
|
118
|
+
return results unless model_class
|
|
119
|
+
|
|
120
|
+
results.map { |row| map_to_model(row, model_class) }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Execute a SELECT query and return the first result.
|
|
124
|
+
#
|
|
125
|
+
# @param query [Query] the query to execute
|
|
126
|
+
# @param model_class [Class, nil] optional model class to map result to
|
|
127
|
+
#
|
|
128
|
+
# @return [Hash, Object, nil] result hash, model instance, or nil if no results
|
|
129
|
+
#
|
|
130
|
+
# @example
|
|
131
|
+
# user = db.first(db.query('users').where('email', 'john@example.com'))
|
|
132
|
+
def first(query, model_class = nil)
|
|
133
|
+
query.limit(1)
|
|
134
|
+
results = @adapter.execute(query.to_sql, query.bindings)
|
|
135
|
+
return nil if results.empty?
|
|
136
|
+
|
|
137
|
+
row = results.first
|
|
138
|
+
model_class ? map_to_model(row, model_class) : row
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Execute an insert query and return the last insert ID
|
|
142
|
+
def execute_insert(query)
|
|
143
|
+
sql = query.to_sql
|
|
144
|
+
@adapter.execute(sql, query.bindings)
|
|
145
|
+
@adapter.last_insert_id
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Execute an update query and return the number of affected rows
|
|
149
|
+
def execute_update(query)
|
|
150
|
+
sql = query.to_sql
|
|
151
|
+
@adapter.execute(sql, query.bindings)
|
|
152
|
+
@adapter.affected_rows
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Execute a delete query and return the number of affected rows
|
|
156
|
+
def execute_delete(query)
|
|
157
|
+
sql = query.to_sql
|
|
158
|
+
@adapter.execute(sql, query.bindings)
|
|
159
|
+
@adapter.affected_rows
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Execute a query and return a scalar value (first column of first row)
|
|
163
|
+
# Useful for aggregate queries like COUNT, SUM, AVG, etc.
|
|
164
|
+
def execute_scalar(query)
|
|
165
|
+
result = first(query)
|
|
166
|
+
return nil if result.nil?
|
|
167
|
+
result.is_a?(Hash) ? result.values.first : result
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Raw SQL with optional model mapping
|
|
171
|
+
def raw(sql, *bindings, model_class: nil)
|
|
172
|
+
results = @adapter.execute(sql, bindings.flatten)
|
|
173
|
+
return results unless model_class
|
|
174
|
+
|
|
175
|
+
results.map { |row| map_to_model(row, model_class) }
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Transaction support
|
|
179
|
+
def transaction
|
|
180
|
+
@adapter.begin_transaction
|
|
181
|
+
result = yield
|
|
182
|
+
@adapter.commit
|
|
183
|
+
result
|
|
184
|
+
rescue => e
|
|
185
|
+
@adapter.rollback
|
|
186
|
+
raise e
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
private
|
|
190
|
+
|
|
191
|
+
# Map a hash to a model instance
|
|
192
|
+
def map_to_model(hash, model_class)
|
|
193
|
+
# Convert string keys to symbols for better Ruby convention
|
|
194
|
+
symbolized = hash.transform_keys(&:to_sym)
|
|
195
|
+
|
|
196
|
+
# Try different initialization strategies
|
|
197
|
+
if model_class.instance_method(:initialize).arity == 0
|
|
198
|
+
# No-arg constructor - set attributes after creation
|
|
199
|
+
instance = model_class.new
|
|
200
|
+
symbolized.each do |key, value|
|
|
201
|
+
setter = "#{key}="
|
|
202
|
+
instance.send(setter, value) if instance.respond_to?(setter)
|
|
203
|
+
end
|
|
204
|
+
instance
|
|
205
|
+
else
|
|
206
|
+
# Constructor accepts hash
|
|
207
|
+
model_class.new(symbolized)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QueryKit
|
|
4
|
+
# DeleteQuery class for building SQL DELETE statements.
|
|
5
|
+
class DeleteQuery
|
|
6
|
+
attr_reader :table, :wheres, :bindings
|
|
7
|
+
|
|
8
|
+
# Initialize a new DeleteQuery instance.
|
|
9
|
+
def initialize(table = nil)
|
|
10
|
+
@table = table
|
|
11
|
+
@wheres = []
|
|
12
|
+
@bindings = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Set the table to delete from.
|
|
16
|
+
def from(table)
|
|
17
|
+
@table = table
|
|
18
|
+
self
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Add a WHERE condition to the delete query.
|
|
22
|
+
def where(column, operator = nil, value = nil)
|
|
23
|
+
if value.nil? && !operator.nil?
|
|
24
|
+
value = operator
|
|
25
|
+
operator = '='
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
@wheres << { type: 'basic', column: column, operator: operator, value: value, boolean: 'AND' }
|
|
29
|
+
@bindings << value
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Generate the SQL DELETE statement.
|
|
34
|
+
def to_sql
|
|
35
|
+
raise "No table specified" unless @table
|
|
36
|
+
|
|
37
|
+
sql = []
|
|
38
|
+
sql << "DELETE FROM #{@table}"
|
|
39
|
+
|
|
40
|
+
unless @wheres.empty?
|
|
41
|
+
sql << "WHERE"
|
|
42
|
+
where_clauses = @wheres.map { |w| "#{w[:column]} #{w[:operator]} ?" }
|
|
43
|
+
sql << where_clauses.join(' AND ')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
sql.join(' ')
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Return the SQL DELETE statement as a string.
|
|
50
|
+
def to_s
|
|
51
|
+
to_sql
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../case_builder'
|
|
4
|
+
|
|
5
|
+
module QueryKit
|
|
6
|
+
# Extension module that adds CASE WHEN support to Query
|
|
7
|
+
# Include this in Query to enable select_case functionality
|
|
8
|
+
module CaseWhenExtension
|
|
9
|
+
# Start a CASE expression and return the builder
|
|
10
|
+
# The builder can be passed to select() like any other expression
|
|
11
|
+
# @param column [String, nil] Optional column for simple CASE
|
|
12
|
+
# @return [CaseBuilder] Builder for constructing CASE expression
|
|
13
|
+
def select_case(column = nil)
|
|
14
|
+
CaseBuilder.new(column)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Override select to handle CaseBuilder objects
|
|
18
|
+
def select(*columns)
|
|
19
|
+
columns.flatten.each do |col|
|
|
20
|
+
if col.is_a?(CaseBuilder)
|
|
21
|
+
@selects << col.to_sql
|
|
22
|
+
@bindings.concat(col.bindings)
|
|
23
|
+
else
|
|
24
|
+
@selects << col
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QueryKit
|
|
4
|
+
# InsertQuery class for building INSERT SQL queries
|
|
5
|
+
class InsertQuery
|
|
6
|
+
attr_reader :table, :values, :bindings
|
|
7
|
+
|
|
8
|
+
# Initialize a new InsertQuery instance.
|
|
9
|
+
def initialize(table = nil)
|
|
10
|
+
@table = table
|
|
11
|
+
@values = []
|
|
12
|
+
@bindings = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Set the table to insert into.
|
|
16
|
+
def into(table)
|
|
17
|
+
@table = table
|
|
18
|
+
self
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Set the table to insert into.
|
|
22
|
+
def values(data)
|
|
23
|
+
if data.is_a?(Hash)
|
|
24
|
+
@values << data
|
|
25
|
+
elsif data.is_a?(Array)
|
|
26
|
+
@values.concat(data)
|
|
27
|
+
end
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Generate the SQL INSERT statement.
|
|
32
|
+
def to_sql
|
|
33
|
+
raise "No table specified" unless @table
|
|
34
|
+
raise "No values specified" if @values.empty?
|
|
35
|
+
|
|
36
|
+
first_row = @values.first
|
|
37
|
+
columns = first_row.keys
|
|
38
|
+
@bindings = @values.flat_map { |row| columns.map { |col| row[col] } }
|
|
39
|
+
|
|
40
|
+
sql = []
|
|
41
|
+
sql << "INSERT INTO #{@table}"
|
|
42
|
+
sql << "(#{columns.join(', ')})"
|
|
43
|
+
sql << "VALUES"
|
|
44
|
+
|
|
45
|
+
value_sets = @values.map do |row|
|
|
46
|
+
placeholders = (['?'] * columns.size).join(', ')
|
|
47
|
+
"(#{placeholders})"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
sql << value_sets.join(', ')
|
|
51
|
+
sql.join(' ')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def to_s
|
|
55
|
+
to_sql
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|