ruflet_record 0.0.1

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,292 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RufletRecord
4
+ class WhereChain
5
+ def initialize(relation)
6
+ @relation = relation
7
+ end
8
+
9
+ def not(conditions, *binds)
10
+ fragment = SQL.predicate(@relation.table_name, conditions, binds)
11
+ @relation.add_where(SQL.negate(fragment))
12
+ end
13
+ end
14
+
15
+ class Relation
16
+ include Enumerable
17
+
18
+ attr_reader :model, :table_name
19
+
20
+ def initialize(model, values = nil)
21
+ @model = model
22
+ @table_name = model.table_name
23
+ @values = values || {
24
+ where: [], order: [], select: [], joins: [], limit: nil,
25
+ offset: nil, distinct: false
26
+ }
27
+ @loaded = false
28
+ @records = nil
29
+ end
30
+
31
+ def where(conditions = nil, *binds)
32
+ return WhereChain.new(self) if conditions.nil?
33
+ add_where(SQL.predicate(@table_name, conditions, binds))
34
+ end
35
+
36
+ def add_where(fragment)
37
+ spawn_with(:where, @values[:where] + [fragment])
38
+ end
39
+
40
+ def order(*values)
41
+ additions = values.flatten
42
+ SQL.order(@table_name, additions)
43
+ spawn_with(:order, @values[:order] + additions)
44
+ end
45
+
46
+ def reorder(*values)
47
+ replacements = values.flatten
48
+ SQL.order(@table_name, replacements)
49
+ spawn_with(:order, replacements)
50
+ end
51
+
52
+ def limit(value)
53
+ number = value.nil? ? nil : value.to_i
54
+ raise ArgumentError, "limit must not be negative" if number && number < 0
55
+ spawn_with(:limit, number)
56
+ end
57
+
58
+ def offset(value)
59
+ number = value.nil? ? nil : value.to_i
60
+ raise ArgumentError, "offset must not be negative" if number && number < 0
61
+ spawn_with(:offset, number)
62
+ end
63
+
64
+ def select(*columns)
65
+ spawn_with(:select, columns.flatten)
66
+ end
67
+
68
+ def distinct(value = true)
69
+ spawn_with(:distinct, !!value)
70
+ end
71
+
72
+ def joins(sql)
73
+ value = sql.is_a?(SQL::Literal) ? sql.sql : sql.to_s
74
+ raise ArgumentError, "joins requires an explicit JOIN expression" unless value =~ /\A\s*(INNER|LEFT|RIGHT|CROSS)?\s*JOIN\s+/i
75
+ spawn_with(:joins, @values[:joins] + [value])
76
+ end
77
+
78
+ def merge(other)
79
+ raise ArgumentError, "relations must have the same model" unless other.model == @model
80
+ merged = duplicate_values
81
+ other_values = other.send(:values)
82
+ merged[:where] += other_values[:where]
83
+ merged[:order] += other_values[:order]
84
+ merged[:joins] += other_values[:joins]
85
+ merged[:select] = other_values[:select] unless other_values[:select].empty?
86
+ merged[:limit] = other_values[:limit] unless other_values[:limit].nil?
87
+ merged[:offset] = other_values[:offset] unless other_values[:offset].nil?
88
+ merged[:distinct] ||= other_values[:distinct]
89
+ self.class.new(@model, merged)
90
+ end
91
+
92
+ def each(&block)
93
+ return to_enum(:each) unless block
94
+ to_a.each(&block)
95
+ end
96
+
97
+ def to_a
98
+ load unless @loaded
99
+ @records.dup
100
+ end
101
+
102
+ def load
103
+ sql, binds = to_sql_and_binds
104
+ @records = @model.connection.select_all(sql, binds).map { |row| @model.instantiate(row) }
105
+ @loaded = true
106
+ self
107
+ end
108
+
109
+ def loaded?
110
+ @loaded
111
+ end
112
+
113
+ def reload
114
+ @loaded = false
115
+ @records = nil
116
+ load
117
+ end
118
+
119
+ def first(count = nil)
120
+ return limit(count).to_a if count
121
+ limit(1).to_a.first
122
+ end
123
+
124
+ def last(count = nil)
125
+ primary = @model.primary_key
126
+ relation = reorder(primary => :desc)
127
+ records = count ? relation.limit(count).to_a.reverse : relation.limit(1).to_a.first
128
+ records
129
+ end
130
+
131
+ def find(id)
132
+ record = where(@model.primary_key => id).first
133
+ raise RecordNotFound, "Couldn't find #{@model.name} with '#{@model.primary_key}'=#{id}" unless record
134
+ record
135
+ end
136
+
137
+ def find_by(conditions)
138
+ where(conditions).first
139
+ end
140
+
141
+ def find_by!(conditions)
142
+ record = find_by(conditions)
143
+ raise RecordNotFound, "Couldn't find #{@model.name}" unless record
144
+ record
145
+ end
146
+
147
+ def count(column = nil)
148
+ aggregate("COUNT", column || SQL::Literal.new("*"), integer: true)
149
+ end
150
+
151
+ def sum(column)
152
+ aggregate("SUM", column) || 0
153
+ end
154
+
155
+ def average(column)
156
+ aggregate("AVG", column)
157
+ end
158
+
159
+ def minimum(column)
160
+ aggregate("MIN", column)
161
+ end
162
+
163
+ def maximum(column)
164
+ aggregate("MAX", column)
165
+ end
166
+
167
+ def exists?(conditions = nil)
168
+ relation = conditions.nil? ? self : where(conditions)
169
+ !relation.select(SQL::Literal.new("1")).limit(1).send(:pluck_value).nil?
170
+ end
171
+
172
+ def pluck(*columns)
173
+ names = columns.flatten
174
+ rows = select(*names).send(:raw_rows)
175
+ return rows.map { |row| row[names.first.to_s] } if names.length == 1
176
+ rows.map { |row| names.map { |name| row[name.to_s] } }
177
+ end
178
+
179
+ def pick(*columns)
180
+ values = limit(1).pluck(*columns)
181
+ values.first
182
+ end
183
+
184
+ def ids
185
+ pluck(@model.primary_key)
186
+ end
187
+
188
+ def create(attributes = {})
189
+ record = @model.new(scope_for_create.merge(attributes))
190
+ record.save
191
+ record
192
+ end
193
+
194
+ def create!(attributes = {})
195
+ record = @model.new(scope_for_create.merge(attributes))
196
+ record.save!
197
+ record
198
+ end
199
+
200
+ def update_all(attributes)
201
+ raise ArgumentError, "attributes must not be empty" if attributes.empty?
202
+ sql, binds = SQL.update(@table_name, stringify_keys(attributes), @values[:where])
203
+ @model.connection.update(sql, binds)
204
+ end
205
+
206
+ def delete_all
207
+ sql, binds = SQL.delete(@table_name, @values[:where])
208
+ @model.connection.delete(sql, binds)
209
+ end
210
+
211
+ def destroy_all
212
+ to_a.each(&:destroy)
213
+ end
214
+
215
+ def find_each(batch_size: 1000)
216
+ return to_enum(:find_each, batch_size: batch_size) unless block_given?
217
+ cursor = nil
218
+ loop do
219
+ relation = reorder(@model.primary_key => :asc).limit(batch_size)
220
+ relation = relation.where("#{SQL.column(@table_name, @model.primary_key)} > ?", cursor) if cursor
221
+ batch = relation.to_a
222
+ break if batch.empty?
223
+ batch.each { |record| yield record }
224
+ cursor = batch.last.public_send(@model.primary_key)
225
+ end
226
+ end
227
+
228
+ def to_sql
229
+ to_sql_and_binds.first
230
+ end
231
+
232
+ def bound_attributes
233
+ to_sql_and_binds.last
234
+ end
235
+
236
+ protected
237
+
238
+ attr_reader :values
239
+
240
+ def raw_rows
241
+ sql, binds = to_sql_and_binds
242
+ @model.connection.select_all(sql, binds)
243
+ end
244
+
245
+ def pluck_value
246
+ row = raw_rows.first
247
+ row && row.values.first
248
+ end
249
+
250
+ private
251
+
252
+ def aggregate(function, column, options = {})
253
+ expression = column.is_a?(SQL::Literal) ? column.sql : SQL.column(@table_name, column)
254
+ row = select(SQL::Literal.new("#{function}(#{expression}) AS value")).reorder.send(:raw_rows).first
255
+ value = row && row["value"]
256
+ options[:integer] && value ? value.to_i : value
257
+ end
258
+
259
+ def to_sql_and_binds
260
+ SQL.select(@table_name, @values)
261
+ end
262
+
263
+ def spawn_with(key, value)
264
+ values = duplicate_values
265
+ values[key] = value
266
+ self.class.new(@model, values)
267
+ end
268
+
269
+ def duplicate_values
270
+ {
271
+ where: @values[:where].dup, order: @values[:order].dup,
272
+ select: @values[:select].dup, joins: @values[:joins].dup,
273
+ limit: @values[:limit], offset: @values[:offset], distinct: @values[:distinct]
274
+ }
275
+ end
276
+
277
+ def stringify_keys(attributes)
278
+ result = {}
279
+ attributes.each { |key, value| result[key.to_s] = value }
280
+ result
281
+ end
282
+
283
+ def scope_for_create
284
+ attributes = {}
285
+ @values[:where].each do |fragment|
286
+ next unless fragment.sql =~ /\A\"[^\"]+\"\.\"([^\"]+)\" = \?\z/
287
+ attributes[Regexp.last_match(1)] = fragment.binds.first
288
+ end
289
+ attributes
290
+ end
291
+ end
292
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RufletRecord
4
+ class TableDefinition
5
+ attr_reader :name, :columns, :indexes, :foreign_keys
6
+
7
+ def initialize(name, id)
8
+ @name = name.to_s
9
+ @columns = []
10
+ @indexes = []
11
+ @foreign_keys = []
12
+ integer(:id, primary_key: true, auto_increment: true, null: false) unless id == false
13
+ end
14
+
15
+ def column(name, type, options = {})
16
+ @columns << [name.to_s, type.to_sym, options]
17
+ self
18
+ end
19
+
20
+ def string(name, options = {}); column(name, :string, options); end
21
+ def text(name, options = {}); column(name, :text, options); end
22
+ def integer(name, options = {}); column(name, :integer, options); end
23
+ def float(name, options = {}); column(name, :float, options); end
24
+ def decimal(name, options = {}); column(name, :decimal, options); end
25
+ def boolean(name, options = {}); column(name, :boolean, options); end
26
+ def datetime(name, options = {}); column(name, :datetime, options); end
27
+ def date(name, options = {}); column(name, :date, options); end
28
+ def binary(name, options = {}); column(name, :binary, options); end
29
+ def json(name, options = {}); column(name, :json, options); end
30
+
31
+ def references(name, options = {})
32
+ column_options = options.dup
33
+ foreign_key = column_options.delete(:foreign_key)
34
+ index_option = column_options.delete(:index)
35
+ column_name = "#{name}_id"
36
+ column(column_name, :integer, column_options)
37
+ index(column_name, index_option.is_a?(Hash) ? index_option : {}) unless index_option == false
38
+ add_foreign_key(column_name, name, foreign_key) if foreign_key
39
+ end
40
+ alias belongs_to references
41
+
42
+ def timestamps(options = {})
43
+ datetime(:created_at, options)
44
+ datetime(:updated_at, options)
45
+ end
46
+
47
+ def index(columns, options = {})
48
+ @indexes << [Array(columns).map(&:to_s), options]
49
+ self
50
+ end
51
+
52
+ def add_foreign_key(column_name, reference_name, options)
53
+ settings = options.is_a?(Hash) ? options : {}
54
+ target_table = settings[:to_table] || Inflector.pluralize(reference_name.to_s)
55
+ target_column = settings[:primary_key] || "id"
56
+ @foreign_keys << [
57
+ column_name.to_s,
58
+ target_table.to_s,
59
+ target_column.to_s,
60
+ settings[:on_delete],
61
+ settings[:on_update]
62
+ ]
63
+ self
64
+ end
65
+ end
66
+
67
+ class Schema
68
+ TYPE_SQL = {
69
+ string: "VARCHAR", text: "TEXT", integer: "INTEGER", float: "REAL",
70
+ decimal: "DECIMAL", boolean: "BOOLEAN", datetime: "DATETIME",
71
+ date: "DATE", binary: "BLOB", json: "TEXT"
72
+ }.freeze
73
+
74
+ class << self
75
+ attr_writer :connection
76
+
77
+ def connection
78
+ @connection || RufletRecord.connection
79
+ end
80
+
81
+ def define(&block)
82
+ schema = new(connection)
83
+ schema.instance_eval(&block)
84
+ schema
85
+ end
86
+ end
87
+
88
+ def initialize(connection = nil)
89
+ @connection = connection || RufletRecord.connection
90
+ end
91
+
92
+ def create_table(name, options = {}, &block)
93
+ definition = TableDefinition.new(name, options.fetch(:id, true))
94
+ block.call(definition) if block
95
+ if options[:force]
96
+ drop_table(name, if_exists: true)
97
+ elsif options[:if_not_exists] && @connection.table_exists?(name)
98
+ return
99
+ end
100
+ columns = definition.columns.map { |column| column_sql(*column) }
101
+ definition.foreign_keys.each { |foreign_key| columns << foreign_key_sql(*foreign_key) }
102
+ @connection.execute("CREATE TABLE #{SQL.quote_identifier(name)} (#{columns.join(', ')})")
103
+ definition.indexes.each { |columns_value, index_options| add_index(name, columns_value, index_options) }
104
+ reset_models
105
+ end
106
+
107
+ def drop_table(name, options = {})
108
+ clause = options[:if_exists] ? " IF EXISTS" : ""
109
+ @connection.execute("DROP TABLE#{clause} #{SQL.quote_identifier(name)}")
110
+ reset_models
111
+ end
112
+
113
+ def add_column(table, name, type, options = {})
114
+ @connection.execute("ALTER TABLE #{SQL.quote_identifier(table)} ADD COLUMN #{column_sql(name, type, options)}")
115
+ reset_models
116
+ end
117
+
118
+ def add_index(table, columns, options = {})
119
+ names = Array(columns).map(&:to_s)
120
+ index_name = options[:name] || "index_#{table}_on_#{names.join('_and_')}"
121
+ unique = options[:unique] ? "UNIQUE " : ""
122
+ quoted_columns = names.map { |name| SQL.quote_identifier(name) }.join(", ")
123
+ @connection.execute("CREATE #{unique}INDEX #{SQL.quote_identifier(index_name)} ON #{SQL.quote_identifier(table)} (#{quoted_columns})")
124
+ end
125
+
126
+ def remove_index(table, columns = nil, options = {})
127
+ names = Array(columns).map(&:to_s)
128
+ index_name = options[:name] || "index_#{table}_on_#{names.join('_and_')}"
129
+ @connection.execute("DROP INDEX #{SQL.quote_identifier(index_name)}")
130
+ end
131
+
132
+ def rename_table(old_name, new_name)
133
+ @connection.execute("ALTER TABLE #{SQL.quote_identifier(old_name)} RENAME TO #{SQL.quote_identifier(new_name)}")
134
+ reset_models
135
+ end
136
+
137
+ private
138
+
139
+ def column_sql(name, type, options)
140
+ sql_type = TYPE_SQL[type.to_sym]
141
+ raise ArgumentError, "unknown column type: #{type}" unless sql_type
142
+ sql = "#{SQL.quote_identifier(name)} #{sql_type}"
143
+ if options[:primary_key]
144
+ sql << " PRIMARY KEY"
145
+ sql << " AUTOINCREMENT" if options[:auto_increment]
146
+ end
147
+ sql << " NOT NULL" if options[:null] == false
148
+ sql << " UNIQUE" if options[:unique]
149
+ sql << " DEFAULT #{quote_default(options[:default])}" if options.key?(:default)
150
+ sql
151
+ end
152
+
153
+ def quote_default(value)
154
+ return "NULL" if value.nil?
155
+ return value.to_s if value.is_a?(Numeric)
156
+ return value ? "1" : "0" if value == true || value == false
157
+ return value.sql if value.is_a?(SQL::Literal)
158
+
159
+ "'#{value.to_s.gsub("'", "''")}'"
160
+ end
161
+
162
+ def foreign_key_sql(column, target_table, target_column, on_delete, on_update)
163
+ sql = "FOREIGN KEY (#{SQL.quote_identifier(column)}) REFERENCES #{SQL.quote_identifier(target_table)} (#{SQL.quote_identifier(target_column)})"
164
+ sql << " ON DELETE #{foreign_key_action(on_delete)}" if on_delete
165
+ sql << " ON UPDATE #{foreign_key_action(on_update)}" if on_update
166
+ sql
167
+ end
168
+
169
+ def foreign_key_action(value)
170
+ action = value.to_s.upcase.tr("_", " ")
171
+ allowed = ["CASCADE", "RESTRICT", "SET NULL", "SET DEFAULT", "NO ACTION"]
172
+ raise ArgumentError, "invalid foreign key action: #{value}" unless allowed.include?(action)
173
+ action
174
+ end
175
+
176
+ def reset_models
177
+ Base.descendants.each(&:reset_column_information) if RufletRecord.const_defined?(:Base)
178
+ end
179
+ end
180
+
181
+ class Migration
182
+ class << self
183
+ attr_accessor :connection
184
+
185
+ def migrate(direction = :up)
186
+ migration = new
187
+ if migration.respond_to?(direction)
188
+ migration.public_send(direction)
189
+ elsif direction.to_sym == :up && migration.respond_to?(:change)
190
+ migration.change
191
+ else
192
+ raise Error, "migration does not implement #{direction}"
193
+ end
194
+ end
195
+ end
196
+
197
+ def connection
198
+ self.class.connection || RufletRecord.connection
199
+ end
200
+
201
+ def method_missing(name, *args, &block)
202
+ schema = Schema.new(connection)
203
+ return schema.public_send(name, *args, &block) if schema.respond_to?(name)
204
+ super
205
+ end
206
+
207
+ def respond_to_missing?(name, include_private = false)
208
+ Schema.new(connection).respond_to?(name, include_private) || super
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RufletRecord
4
+ module SQL
5
+ class Literal
6
+ attr_reader :sql
7
+
8
+ def initialize(sql)
9
+ @sql = sql.to_s.freeze
10
+ end
11
+
12
+ def to_s
13
+ @sql
14
+ end
15
+ end
16
+
17
+ class Fragment
18
+ attr_reader :sql, :binds
19
+
20
+ def initialize(sql, binds)
21
+ @sql = sql.to_s.freeze
22
+ @binds = binds.freeze
23
+ end
24
+ end
25
+
26
+ class << self
27
+
28
+ def quote_identifier(name)
29
+ parts = name.to_s.split(".")
30
+ parts.map { |part| "\"#{part.gsub('"', '""')}\"" }.join(".")
31
+ end
32
+
33
+ def column(table, name)
34
+ return name.sql if name.is_a?(Literal)
35
+
36
+ value = name.to_s
37
+ value.include?(".") ? quote_identifier(value) : "#{quote_identifier(table)}.#{quote_identifier(value)}"
38
+ end
39
+
40
+ def columns(table, values)
41
+ selected = values.nil? || values.empty? ? [Literal.new("#{quote_identifier(table)}.*")] : values
42
+ selected.map { |value| column(table, value) }.join(", ")
43
+ end
44
+
45
+ def predicate(table, conditions, binds)
46
+ if conditions.is_a?(Fragment)
47
+ return conditions
48
+ elsif conditions.is_a?(Hash)
49
+ parts = []
50
+ values = []
51
+ conditions.each do |name, value|
52
+ identifier = column(table, name)
53
+ if value.nil?
54
+ parts << "#{identifier} IS NULL"
55
+ elsif value.is_a?(Array)
56
+ if value.empty?
57
+ parts << "1 = 0"
58
+ else
59
+ parts << "#{identifier} IN (#{(["?"] * value.length).join(', ')})"
60
+ values.concat(value)
61
+ end
62
+ elsif value.is_a?(Range)
63
+ if value.exclude_end?
64
+ parts << "(#{identifier} >= ? AND #{identifier} < ?)"
65
+ else
66
+ parts << "#{identifier} BETWEEN ? AND ?"
67
+ end
68
+ values << value.begin << value.end
69
+ else
70
+ parts << "#{identifier} = ?"
71
+ values << value
72
+ end
73
+ end
74
+ return Fragment.new(parts.empty? ? "1 = 1" : parts.join(" AND "), values)
75
+ elsif conditions.is_a?(String)
76
+ expected = conditions.count("?")
77
+ if expected != binds.length
78
+ raise ArgumentError, "wrong number of bind values (#{binds.length} for #{expected})"
79
+ end
80
+ return Fragment.new(conditions, binds)
81
+ end
82
+
83
+ raise ArgumentError, "where conditions must be a Hash, String, or SQL fragment"
84
+ end
85
+
86
+ def negate(fragment)
87
+ Fragment.new("NOT (#{fragment.sql})", fragment.binds)
88
+ end
89
+
90
+ def order(table, values)
91
+ values.map do |value|
92
+ if value.is_a?(Hash)
93
+ value.map do |name, direction|
94
+ normalized = direction.to_s.upcase
95
+ raise ArgumentError, "invalid order direction: #{direction}" unless %w[ASC DESC].include?(normalized)
96
+ "#{column(table, name)} #{normalized}"
97
+ end.join(", ")
98
+ elsif value.is_a?(Symbol)
99
+ "#{column(table, value)} ASC"
100
+ elsif value.is_a?(Literal)
101
+ value.sql
102
+ else
103
+ parse_order_string(table, value.to_s)
104
+ end
105
+ end.join(", ")
106
+ end
107
+
108
+ def parse_order_string(table, value)
109
+ value.split(",").map do |piece|
110
+ match = /\A\s*([A-Za-z_][A-Za-z0-9_.]*)(?:\s+(ASC|DESC))?\s*\z/i.match(piece)
111
+ raise ArgumentError, "unsafe order expression: #{value.inspect}" unless match
112
+ direction = match[2] ? " #{match[2].upcase}" : " ASC"
113
+ "#{column(table, match[1])}#{direction}"
114
+ end.join(", ")
115
+ end
116
+
117
+ def select(table, values)
118
+ sql = +"SELECT "
119
+ sql << "DISTINCT " if values[:distinct]
120
+ sql << columns(table, values[:select])
121
+ sql << " FROM #{quote_identifier(table)}"
122
+ sql << " #{values[:joins].join(' ')}" unless values[:joins].empty?
123
+ binds = []
124
+ append_wheres(sql, binds, values[:where])
125
+ unless values[:order].empty?
126
+ sql << " ORDER BY #{order(table, values[:order])}"
127
+ end
128
+ if values[:limit]
129
+ sql << " LIMIT ?"
130
+ binds << values[:limit]
131
+ elsif values[:offset]
132
+ sql << " LIMIT -1"
133
+ end
134
+ if values[:offset]
135
+ sql << " OFFSET ?"
136
+ binds << values[:offset]
137
+ end
138
+ [sql, binds]
139
+ end
140
+
141
+ def insert(table, attributes)
142
+ names = attributes.keys
143
+ sql = "INSERT INTO #{quote_identifier(table)}"
144
+ if names.empty?
145
+ return ["#{sql} DEFAULT VALUES", []]
146
+ end
147
+ quoted = names.map { |name| quote_identifier(name) }.join(", ")
148
+ placeholders = (["?"] * names.length).join(", ")
149
+ ["#{sql} (#{quoted}) VALUES (#{placeholders})", names.map { |name| attributes[name] }]
150
+ end
151
+
152
+ def update(table, attributes, wheres)
153
+ assignments = attributes.keys.map { |name| "#{quote_identifier(name)} = ?" }.join(", ")
154
+ binds = attributes.keys.map { |name| attributes[name] }
155
+ sql = "UPDATE #{quote_identifier(table)} SET #{assignments}"
156
+ append_wheres(sql, binds, wheres)
157
+ [sql, binds]
158
+ end
159
+
160
+ def delete(table, wheres)
161
+ sql = "DELETE FROM #{quote_identifier(table)}"
162
+ binds = []
163
+ append_wheres(sql, binds, wheres)
164
+ [sql, binds]
165
+ end
166
+
167
+ def append_wheres(sql, binds, wheres)
168
+ return if wheres.empty?
169
+
170
+ sql << " WHERE #{wheres.map { |fragment| "(#{fragment.sql})" }.join(' AND ')}"
171
+ wheres.each { |fragment| binds.concat(fragment.binds) }
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RufletRecord
4
+ VERSION = "0.0.1"
5
+ end