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.
@@ -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