edgeway 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +34 -0
- data/README.md +270 -9
- data/js/cloudflare-d1-sessions.js +68 -0
- data/js/cloudflare.js +123 -22
- data/js/cors.js +15 -16
- data/js/headers.js +16 -0
- data/js/jwt.js +1 -1
- data/lib/edgeway/application.rb +6 -3
- data/lib/edgeway/cli/build/app_config.rb +39 -0
- data/lib/edgeway/cli/build/app_files.rb +40 -0
- data/lib/edgeway/cli/build/database_config.rb +92 -0
- data/lib/edgeway/cli/build/view_compiler.rb +191 -0
- data/lib/edgeway/cli/build.rb +36 -11
- data/lib/edgeway/cli/generate.rb +174 -26
- data/lib/edgeway/cli/new.rb +1 -1
- data/lib/edgeway/cli/test.rb +4 -1
- data/lib/edgeway/cli.rb +5 -4
- data/lib/edgeway/controller/base.rb +12 -2
- data/lib/edgeway/controller/dispatcher.rb +11 -1
- data/lib/edgeway/controller/rendering.rb +24 -4
- data/lib/edgeway/db/adapter.rb +38 -0
- data/lib/edgeway/db/memory.rb +9 -2
- data/lib/edgeway/db/query_stats.rb +46 -0
- data/lib/edgeway/db/sqlite.rb +73 -9
- data/lib/edgeway/errors.rb +22 -0
- data/lib/edgeway/generators/app/templates/.github/workflows/ci.yml.tt +38 -0
- data/lib/edgeway/generators/app/templates/.ruby-version.tt +1 -0
- data/lib/edgeway/generators/app/templates/README.md.tt +6 -2
- data/lib/edgeway/generators/app/templates/app/views/layouts/application.html.erb +11 -0
- data/lib/edgeway/generators/app/templates/bin/build.tt +1 -1
- data/lib/edgeway/generators/app/templates/bin/deploy.tt +1 -1
- data/lib/edgeway/generators/app/templates/bin/test.tt +7 -0
- data/lib/edgeway/generators/app/templates/config/database.yml +17 -0
- data/lib/edgeway/generators/app/templates/mise.toml.tt +1 -1
- data/lib/edgeway/generators/app/templates/wrangler.toml.tt +7 -0
- data/lib/edgeway/http/auth/stub.rb +1 -3
- data/lib/edgeway/http/request.rb +2 -22
- data/lib/edgeway/inflector.rb +19 -0
- data/lib/edgeway/model/attributes.rb +93 -10
- data/lib/edgeway/model/base.rb +50 -5
- data/lib/edgeway/model/connection_registry.rb +25 -0
- data/lib/edgeway/model/macros.rb +5 -1
- data/lib/edgeway/model/persistence.rb +18 -11
- data/lib/edgeway/model/relation.rb +106 -56
- data/lib/edgeway/model/validations.rb +29 -23
- data/lib/edgeway/routing/route.rb +1 -6
- data/lib/edgeway/server.rb +12 -0
- data/lib/edgeway/service/cloudflare/d1.rb +39 -10
- data/lib/edgeway/test.rb +29 -8
- data/lib/edgeway/version.rb +1 -1
- data/lib/edgeway/view.rb +24 -0
- data/lib/edgeway.rb +3 -0
- metadata +17 -4
- data/lib/edgeway/cli/app_config.rb +0 -35
- data/lib/edgeway/cli/app_files.rb +0 -37
|
@@ -8,16 +8,33 @@ module Edgeway
|
|
|
8
8
|
INT_MAX = 2147483647
|
|
9
9
|
INT_MIN = -2147483648
|
|
10
10
|
|
|
11
|
+
PRIMARY = "primary"
|
|
12
|
+
|
|
13
|
+
def self.registry = (@registry ||= ConnectionRegistry.new)
|
|
14
|
+
|
|
15
|
+
def self.connect(name, adapter) = registry.connect(name, adapter)
|
|
16
|
+
|
|
17
|
+
def self.disconnect(name) = registry.disconnect(name)
|
|
18
|
+
|
|
11
19
|
def self.connection=(adapter)
|
|
12
|
-
|
|
20
|
+
connect(PRIMARY, adapter)
|
|
13
21
|
end
|
|
14
22
|
|
|
15
|
-
def self.
|
|
16
|
-
|
|
23
|
+
def self.connected? = registry.connected?(PRIMARY)
|
|
24
|
+
|
|
25
|
+
def self.connection = connection_for(PRIMARY)
|
|
26
|
+
|
|
27
|
+
def self.connection_for(name) = registry.fetch(name)
|
|
28
|
+
|
|
29
|
+
def self.database_names = registry.names
|
|
30
|
+
|
|
31
|
+
def self.query_stats(name = PRIMARY)
|
|
32
|
+
connection_for(name).query_stats
|
|
17
33
|
end
|
|
18
34
|
|
|
19
35
|
# Runs the block with writes queued instead of executed, then sends them
|
|
20
|
-
# all as one atomic batch (
|
|
36
|
+
# all as one atomic batch (one SQL transaction, all or none).
|
|
37
|
+
# A batch belongs to the database of its first write.
|
|
21
38
|
def self.batch
|
|
22
39
|
if @batch_queue
|
|
23
40
|
yield
|
|
@@ -25,14 +42,16 @@ module Edgeway
|
|
|
25
42
|
end
|
|
26
43
|
queue = []
|
|
27
44
|
@batch_queue = queue
|
|
45
|
+
@batch_database = nil
|
|
28
46
|
begin
|
|
29
47
|
yield
|
|
30
48
|
rescue => e
|
|
31
49
|
@batch_queue = nil
|
|
32
50
|
raise e
|
|
33
51
|
end
|
|
52
|
+
database = @batch_database
|
|
34
53
|
@batch_queue = nil
|
|
35
|
-
queue.empty? ? [] :
|
|
54
|
+
queue.empty? ? [] : connection_for(database).batch(queue)
|
|
36
55
|
end
|
|
37
56
|
|
|
38
57
|
def self.batching? = !@batch_queue.nil?
|
|
@@ -40,16 +59,42 @@ module Edgeway
|
|
|
40
59
|
# Every model write but an INSERT goes through here: executed at once, or
|
|
41
60
|
# queued inside batch. A queued write answers a placeholder result, as its
|
|
42
61
|
# counts are not known until the batch is sent.
|
|
43
|
-
def self.write(sql, binds)
|
|
62
|
+
def self.write(sql, binds, database: PRIMARY)
|
|
63
|
+
name = database.to_s
|
|
44
64
|
queue = @batch_queue
|
|
45
65
|
if queue.nil?
|
|
46
|
-
|
|
66
|
+
connection_for(name).execute(sql, binds)
|
|
47
67
|
else
|
|
68
|
+
join_batch(name)
|
|
48
69
|
queue << [sql, binds]
|
|
49
70
|
{ "changes" => 0, "last_insert_id" => 0 }
|
|
50
71
|
end
|
|
51
72
|
end
|
|
52
73
|
|
|
74
|
+
def self.join_batch(name)
|
|
75
|
+
current = @batch_database
|
|
76
|
+
if current.nil?
|
|
77
|
+
@batch_database = name
|
|
78
|
+
elsif current != name
|
|
79
|
+
raise ArgumentError, "Model.batch spans databases (#{current} and #{name}); a batch runs on one database"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.select_all(sql, binds = [], database: PRIMARY)
|
|
84
|
+
connection_for(database).select_all(sql, binds.map { |b| database_value(b) })
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.execute(sql, binds = [], database: PRIMARY)
|
|
88
|
+
write(sql, binds.map { |b| database_value(b) }, database: database)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.exec(sql, database: PRIMARY)
|
|
92
|
+
raise ArgumentError, "Model.exec can't run inside Model.batch" if batching?
|
|
93
|
+
raise ArgumentError, "Model.exec needs SQL text" unless sql.is_a?(String) && !sql.strip.empty?
|
|
94
|
+
connection_for(database).exec(sql)
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
53
98
|
def self.to_int(v)
|
|
54
99
|
n = v.is_a?(Integer) ? v : v.to_s.to_i
|
|
55
100
|
n > INT_MAX || n < INT_MIN ? nil : n
|
|
@@ -63,13 +108,51 @@ module Edgeway
|
|
|
63
108
|
when :integer then to_int(v)
|
|
64
109
|
when :float then v.is_a?(Float) ? v : v.to_s.to_f
|
|
65
110
|
when :boolean then v == true || TRUE_STRINGS.include?(v.to_s)
|
|
111
|
+
when :bigint then to_bigint(v)
|
|
112
|
+
when :binary then to_binary(v)
|
|
66
113
|
else v.to_s
|
|
67
114
|
end
|
|
68
115
|
end
|
|
69
116
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
117
|
+
INT64_MAX_DIGITS = "9223372036854775807"
|
|
118
|
+
INT64_MIN_DIGITS = "9223372036854775808"
|
|
119
|
+
|
|
120
|
+
# A bigint is held as its decimal String, as a number beyond 32 bits can't
|
|
121
|
+
# be an Integer in wasm.
|
|
122
|
+
def self.to_bigint(value)
|
|
123
|
+
text = value.to_s
|
|
124
|
+
return nil unless text.match?(/\A-?[0-9]+\z/)
|
|
125
|
+
negative = text.start_with?("-")
|
|
126
|
+
digits = text.sub(/\A-?0*/, "")
|
|
127
|
+
digits = "0" if digits.empty?
|
|
128
|
+
return nil unless within_int64?(digits, negative)
|
|
129
|
+
negative && digits != "0" ? "-#{digits}" : digits
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def self.within_int64?(digits, negative)
|
|
133
|
+
limit = negative ? INT64_MIN_DIGITS : INT64_MAX_DIGITS
|
|
134
|
+
return digits.length < limit.length || (digits.length == limit.length && digits <= limit)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def self.to_binary(value)
|
|
138
|
+
return value.to_s.b unless value.is_a?(Hash)
|
|
139
|
+
return nil unless value.key?("$blob")
|
|
140
|
+
value["$blob"].to_s.unpack1("m0").b
|
|
141
|
+
rescue ArgumentError
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def self.encode_base64(bytes) = [bytes].pack("m0")
|
|
146
|
+
|
|
147
|
+
def self.blob(bytes)
|
|
148
|
+
{ "$blob" => encode_base64(bytes) }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def self.database_value(value, type = nil)
|
|
152
|
+
return connection.quote_boolean(value) if value == true || value == false
|
|
153
|
+
return value.getutc.iso8601(3) if value.is_a?(Time)
|
|
154
|
+
return blob(value) if type == :binary && value.is_a?(String)
|
|
155
|
+
value
|
|
73
156
|
end
|
|
74
157
|
|
|
75
158
|
def self.underscore(name)
|
data/lib/edgeway/model/base.rb
CHANGED
|
@@ -40,6 +40,26 @@ module Edgeway
|
|
|
40
40
|
@table_name || Inflector.pluralize(Model.underscore(name.split("::").last))
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
def self.database=(name)
|
|
44
|
+
raise ArgumentError, "database name can't be blank" if name.nil? || name.to_s.empty?
|
|
45
|
+
@database = name.to_s
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.own_database = @database
|
|
49
|
+
|
|
50
|
+
def self.database
|
|
51
|
+
klass = self
|
|
52
|
+
while klass
|
|
53
|
+
name = klass.own_database
|
|
54
|
+
return name unless name.nil?
|
|
55
|
+
break if klass == Base
|
|
56
|
+
klass = klass.superclass
|
|
57
|
+
end
|
|
58
|
+
Model::PRIMARY
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.connection = Model.connection_for(database)
|
|
62
|
+
|
|
43
63
|
def self.attribute_types = (@attribute_types ||= {})
|
|
44
64
|
def self.attribute_defaults = (@attribute_defaults ||= {})
|
|
45
65
|
def self.belongs_to_list = (@belongs_to ||= [])
|
|
@@ -82,12 +102,17 @@ module Edgeway
|
|
|
82
102
|
["id"] + attribute_types.keys
|
|
83
103
|
end
|
|
84
104
|
|
|
105
|
+
def self.id_type = (@id_type ||= :integer)
|
|
106
|
+
def self.id_type=(type)
|
|
107
|
+
@id_type = type
|
|
108
|
+
end
|
|
109
|
+
|
|
85
110
|
def self.type_of(col)
|
|
86
|
-
col == "id" ?
|
|
111
|
+
col == "id" ? id_type : attribute_types[col]
|
|
87
112
|
end
|
|
88
113
|
|
|
89
114
|
def self.all = Relation.new(self)
|
|
90
|
-
def self.where(conditions) = all.where(conditions)
|
|
115
|
+
def self.where(conditions, *args) = all.where(conditions, *args)
|
|
91
116
|
def self.order(col, dir = :asc) = all.order(col, dir)
|
|
92
117
|
def self.reorder(col, dir = :asc) = all.reorder(col, dir)
|
|
93
118
|
def self.delete_all = all.delete_all
|
|
@@ -100,6 +125,7 @@ module Edgeway
|
|
|
100
125
|
def self.pluck(col) = all.pluck(col)
|
|
101
126
|
def self.first = all.first
|
|
102
127
|
def self.find_by(conditions) = all.find_by(conditions)
|
|
128
|
+
def self.find_by_sql(sql, binds = []) = Model.select_all(sql, binds, database: database).map { |row| instantiate(row) }
|
|
103
129
|
def self.find(id) = all.find(id)
|
|
104
130
|
|
|
105
131
|
def self.find_or_create_by(conditions) = all.find_or_create_by(conditions)
|
|
@@ -148,16 +174,27 @@ module Edgeway
|
|
|
148
174
|
types = self.class.attribute_types
|
|
149
175
|
values.each do |k, v|
|
|
150
176
|
key = k.to_s
|
|
151
|
-
write_attribute(key, v) if types.key?(key)
|
|
177
|
+
write_attribute(key, param_value(types[key], v, attrs)) if types.key?(key)
|
|
152
178
|
end
|
|
153
179
|
end
|
|
154
180
|
|
|
181
|
+
def param_value(type, value, attrs)
|
|
182
|
+
return value unless type == :binary && attrs.is_a?(HTTP::Parameters) && value.is_a?(String)
|
|
183
|
+
{ "$blob" => value }
|
|
184
|
+
end
|
|
185
|
+
|
|
155
186
|
def load_row(row)
|
|
156
|
-
self.class.columns.each { |col| @attributes[col] = Model.cast(self.class.type_of(col), row[col]) }
|
|
157
187
|
@raw = {}
|
|
188
|
+
self.class.columns.each { |column| load_column(column, row[column]) }
|
|
158
189
|
@persisted = true
|
|
159
190
|
end
|
|
160
191
|
|
|
192
|
+
def load_column(column, value)
|
|
193
|
+
casted = Model.cast(self.class.type_of(column), value)
|
|
194
|
+
@attributes[column] = casted
|
|
195
|
+
@raw[column] = value if casted.nil? && !value.nil?
|
|
196
|
+
end
|
|
197
|
+
|
|
161
198
|
def write_attribute(col, v)
|
|
162
199
|
@raw[col] = v
|
|
163
200
|
@attributes[col] = Model.cast(self.class.type_of(col), v)
|
|
@@ -165,9 +202,17 @@ module Edgeway
|
|
|
165
202
|
|
|
166
203
|
def as_json
|
|
167
204
|
h = {}
|
|
168
|
-
self.class.columns.each { |
|
|
205
|
+
self.class.columns.each { |column| h[column] = json_value(column) }
|
|
169
206
|
h
|
|
170
207
|
end
|
|
208
|
+
|
|
209
|
+
private
|
|
210
|
+
|
|
211
|
+
def json_value(column)
|
|
212
|
+
value = @attributes[column]
|
|
213
|
+
return value unless self.class.type_of(column) == :binary && !value.nil?
|
|
214
|
+
Model.encode_base64(value)
|
|
215
|
+
end
|
|
171
216
|
end
|
|
172
217
|
end
|
|
173
218
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Edgeway
|
|
2
|
+
module Model
|
|
3
|
+
class ConnectionRegistry
|
|
4
|
+
def initialize
|
|
5
|
+
@adapters = {}
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def connect(name, adapter)
|
|
9
|
+
@adapters[name.to_s] = adapter
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def disconnect(name)
|
|
13
|
+
@adapters.delete(name.to_s)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def connected?(name) = @adapters.key?(name.to_s)
|
|
17
|
+
|
|
18
|
+
def names = @adapters.keys
|
|
19
|
+
|
|
20
|
+
def fetch(name)
|
|
21
|
+
@adapters[name.to_s] || raise(ConfigurationError, "no database #{name}: it is not under databases: of this environment in config/database.yml")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/edgeway/model/macros.rb
CHANGED
|
@@ -2,13 +2,17 @@ module Edgeway
|
|
|
2
2
|
module Model
|
|
3
3
|
module Macros
|
|
4
4
|
def attribute(name, type, default: nil)
|
|
5
|
+
if name.to_s == "id"
|
|
6
|
+
self.id_type = type
|
|
7
|
+
return
|
|
8
|
+
end
|
|
5
9
|
attribute_types[name.to_s] = type
|
|
6
10
|
attribute_defaults[name.to_s] = default unless default.nil?
|
|
7
11
|
define_method(name) { @attributes[name.to_s] }
|
|
8
12
|
define_method("#{name}=") { |v| write_attribute(name.to_s, v) }
|
|
9
13
|
end
|
|
10
14
|
|
|
11
|
-
# created_at / updated_at, as ISO 8601 text: SQLite
|
|
15
|
+
# created_at / updated_at, as ISO 8601 text: SQLite has no datetime
|
|
12
16
|
# column type, the migrations declare them TEXT.
|
|
13
17
|
def timestamps
|
|
14
18
|
attribute :created_at, :datetime
|
|
@@ -53,6 +53,7 @@ module Edgeway
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def destroy
|
|
56
|
+
check_dependents_database
|
|
56
57
|
if self.class.dependents.empty?
|
|
57
58
|
delete_row
|
|
58
59
|
else
|
|
@@ -86,10 +87,8 @@ module Edgeway
|
|
|
86
87
|
cfg = nil
|
|
87
88
|
self.class.has_manys.each { |h| cfg = h if h.name == name }
|
|
88
89
|
raise ArgumentError, "no has_many #{name}" if cfg.nil?
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
fk = cfg.foreign_key.nil? ? "#{Edgeway::Model.underscore(self.class.name.split("::").last)}_id" : cfg.foreign_key.dup
|
|
92
|
-
pk = self.class.column(cfg.primary_key.nil? ? "id" : cfg.primary_key.dup)
|
|
90
|
+
fk = cfg.foreign_key.nil? ? "#{Edgeway::Model.underscore(self.class.name.split("::").last)}_id" : cfg.foreign_key
|
|
91
|
+
pk = self.class.column(cfg.primary_key.nil? ? "id" : cfg.primary_key)
|
|
93
92
|
Edgeway::Model::Base.model_named(name, self.class.name).where(fk => @attributes[pk])
|
|
94
93
|
end
|
|
95
94
|
|
|
@@ -108,26 +107,34 @@ module Edgeway
|
|
|
108
107
|
|
|
109
108
|
private
|
|
110
109
|
|
|
110
|
+
def check_dependents_database
|
|
111
|
+
self.class.dependents.each do |dependent|
|
|
112
|
+
child = Edgeway::Model::Base.model_named(dependent.name, self.class.name)
|
|
113
|
+
next if child.database == self.class.database
|
|
114
|
+
raise ArgumentError, "#{self.class.name} has_many :#{dependent.name}, dependent: :#{dependent.mode}, on database #{child.database}; destroy can't delete across databases atomically"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
111
118
|
def delete_row
|
|
112
|
-
conn =
|
|
113
|
-
Model.write("DELETE FROM #{self.class.table_name} WHERE id = #{conn.placeholder(1)}", [id])
|
|
119
|
+
conn = self.class.connection
|
|
120
|
+
Model.write("DELETE FROM #{self.class.table_name} WHERE id = #{conn.placeholder(1)}", [id], database: self.class.database)
|
|
114
121
|
end
|
|
115
122
|
|
|
116
123
|
def update_row
|
|
117
|
-
conn =
|
|
124
|
+
conn = self.class.connection
|
|
118
125
|
cols = self.class.attribute_types.keys
|
|
119
|
-
values = cols.map { |c| Model.
|
|
126
|
+
values = cols.map { |c| Model.database_value(@attributes[c], self.class.type_of(c)) }
|
|
120
127
|
sets = cols.each_with_index.map { |c, i| "#{c} = #{conn.placeholder(i + 1)}" }.join(", ")
|
|
121
128
|
Model.write("UPDATE #{self.class.table_name} SET #{sets} WHERE id = #{conn.placeholder(cols.size + 1)}",
|
|
122
|
-
values + [id])
|
|
129
|
+
values + [id], database: self.class.database)
|
|
123
130
|
end
|
|
124
131
|
|
|
125
132
|
def insert_row
|
|
126
133
|
raise ArgumentError, "create isn't supported inside Model.batch" if Model.batching?
|
|
127
|
-
conn =
|
|
134
|
+
conn = self.class.connection
|
|
128
135
|
table = self.class.table_name
|
|
129
136
|
cols = self.class.attribute_types.keys.reject { |c| @attributes[c].nil? }
|
|
130
|
-
values = cols.map { |c| Model.
|
|
137
|
+
values = cols.map { |c| Model.database_value(@attributes[c], self.class.type_of(c)) }
|
|
131
138
|
sql = "INSERT INTO #{table}"
|
|
132
139
|
if cols.empty?
|
|
133
140
|
sql += " DEFAULT VALUES"
|
|
@@ -3,12 +3,17 @@ module Edgeway
|
|
|
3
3
|
class Relation
|
|
4
4
|
# eager is the associations eager_load names (empty: none); records, when
|
|
5
5
|
# set, are what to_a answers instead of a query (an eager-loaded has_many).
|
|
6
|
+
# conds are the WHERE terms (each with its own "?"s) and binds their
|
|
7
|
+
# values in order; eq_cols/eq_vals are the plain equality terms, which
|
|
8
|
+
# build copies into the new record.
|
|
6
9
|
attr_writer :records
|
|
7
10
|
|
|
8
|
-
def initialize(model,
|
|
11
|
+
def initialize(model, conds = [], binds = [], eq_cols = [], eq_vals = [], order = [], limit = nil, offset = nil, eager = [])
|
|
9
12
|
@model = model
|
|
10
|
-
@
|
|
13
|
+
@conds = conds
|
|
11
14
|
@binds = binds
|
|
15
|
+
@eq_cols = eq_cols
|
|
16
|
+
@eq_vals = eq_vals
|
|
12
17
|
@order = order
|
|
13
18
|
@limit = limit
|
|
14
19
|
@offset = offset
|
|
@@ -16,37 +21,45 @@ module Edgeway
|
|
|
16
21
|
@records = nil
|
|
17
22
|
end
|
|
18
23
|
|
|
19
|
-
def where(conditions)
|
|
20
|
-
|
|
24
|
+
def where(conditions, *args)
|
|
25
|
+
conds = @conds.dup
|
|
21
26
|
binds = @binds.dup
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
eq_cols = @eq_cols.dup
|
|
28
|
+
eq_vals = @eq_vals.dup
|
|
29
|
+
if conditions.is_a?(String)
|
|
30
|
+
add_fragment(conditions, args, conds, binds)
|
|
31
|
+
else
|
|
32
|
+
raise ArgumentError, "where(Hash) takes no binds" unless args.empty?
|
|
33
|
+
conditions.each do |k, v|
|
|
34
|
+
column = @model.column(k)
|
|
35
|
+
type = @model.type_of(column)
|
|
36
|
+
if v.is_a?(Array)
|
|
37
|
+
add_in(column, type, v, conds, binds)
|
|
38
|
+
elsif v.is_a?(Range)
|
|
39
|
+
add_range(column, type, v, conds, binds)
|
|
40
|
+
else
|
|
41
|
+
add_equality(column, type, v, conds, binds, eq_cols, eq_vals)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
31
44
|
end
|
|
32
|
-
Relation.new(@model,
|
|
45
|
+
Relation.new(@model, conds, binds, eq_cols, eq_vals, @order, @limit, @offset, @eager)
|
|
33
46
|
end
|
|
34
47
|
|
|
35
48
|
def order(col, dir = :asc)
|
|
36
|
-
Relation.new(@model, @
|
|
49
|
+
Relation.new(@model, @conds, @binds, @eq_cols, @eq_vals, @order + [order_term(col, dir)], @limit, @offset, @eager)
|
|
37
50
|
end
|
|
38
51
|
|
|
39
52
|
# Replaces all the terms.
|
|
40
53
|
def reorder(col, dir = :asc)
|
|
41
|
-
Relation.new(@model, @
|
|
54
|
+
Relation.new(@model, @conds, @binds, @eq_cols, @eq_vals, [order_term(col, dir)], @limit, @offset, @eager)
|
|
42
55
|
end
|
|
43
56
|
|
|
44
57
|
def limit(n)
|
|
45
|
-
Relation.new(@model, @
|
|
58
|
+
Relation.new(@model, @conds, @binds, @eq_cols, @eq_vals, @order, count_arg("limit", n), @offset, @eager)
|
|
46
59
|
end
|
|
47
60
|
|
|
48
61
|
def offset(n)
|
|
49
|
-
Relation.new(@model, @
|
|
62
|
+
Relation.new(@model, @conds, @binds, @eq_cols, @eq_vals, @order, @limit, count_arg("offset", n), @eager)
|
|
50
63
|
end
|
|
51
64
|
|
|
52
65
|
# Loads the has_manys and belongs_tos named of every record with the one
|
|
@@ -59,12 +72,12 @@ module Edgeway
|
|
|
59
72
|
names.each do |n|
|
|
60
73
|
eager << n.to_s unless eager.include?(n.to_s)
|
|
61
74
|
end
|
|
62
|
-
Relation.new(@model, @
|
|
75
|
+
Relation.new(@model, @conds, @binds, @eq_cols, @eq_vals, @order, @limit, @offset, eager)
|
|
63
76
|
end
|
|
64
77
|
|
|
65
78
|
# Same conditions, but to_a answers these records.
|
|
66
79
|
def loaded(records)
|
|
67
|
-
relation = Relation.new(@model, @
|
|
80
|
+
relation = Relation.new(@model, @conds, @binds, @eq_cols, @eq_vals, @order, @limit, @offset)
|
|
68
81
|
relation.records = records
|
|
69
82
|
relation
|
|
70
83
|
end
|
|
@@ -73,12 +86,12 @@ module Edgeway
|
|
|
73
86
|
return @records unless @records.nil?
|
|
74
87
|
return eager_records unless @eager.empty?
|
|
75
88
|
sql, binds = select(@model.columns.join(", "), true)
|
|
76
|
-
|
|
89
|
+
@model.connection.select_all(sql, binds).map { |row| @model.instantiate(row) }
|
|
77
90
|
end
|
|
78
91
|
|
|
79
92
|
def count
|
|
80
93
|
sql, binds = select("1", false)
|
|
81
|
-
|
|
94
|
+
@model.connection.select_all("SELECT COUNT(*) AS n FROM (#{sql})", binds)[0]["n"].to_i
|
|
82
95
|
end
|
|
83
96
|
|
|
84
97
|
def exists? = limit(1).count > 0
|
|
@@ -87,16 +100,14 @@ module Edgeway
|
|
|
87
100
|
name = @model.column(col)
|
|
88
101
|
type = @model.type_of(name)
|
|
89
102
|
sql, binds = select(name, true)
|
|
90
|
-
|
|
103
|
+
@model.connection.select_all(sql, binds).map { |row| Model.cast(type, row[name]) }
|
|
91
104
|
end
|
|
92
105
|
|
|
93
106
|
def first = (@order.empty? ? order(:id) : self).limit(1).to_a.first
|
|
94
107
|
|
|
95
108
|
def delete_all
|
|
96
109
|
raise ArgumentError, "delete_all doesn't support limit/offset" if @limit || @offset
|
|
97
|
-
|
|
98
|
-
where_sql, _marks = where_clause(conn)
|
|
99
|
-
Model.write("DELETE FROM #{@model.table_name}#{where_sql}", @binds.compact)["changes"]
|
|
110
|
+
Model.write("DELETE FROM #{@model.table_name}#{where_clause}", @binds, database: @model.database)["changes"]
|
|
100
111
|
end
|
|
101
112
|
|
|
102
113
|
def destroy_all
|
|
@@ -110,7 +121,7 @@ module Edgeway
|
|
|
110
121
|
def size = to_a.size
|
|
111
122
|
def empty? = !exists?
|
|
112
123
|
|
|
113
|
-
def find_by(conditions) = Relation.new(@model, @
|
|
124
|
+
def find_by(conditions) = Relation.new(@model, @conds, @binds, @eq_cols, @eq_vals, @order, @limit, nil, @eager).where(conditions).limit(1).to_a.first
|
|
114
125
|
|
|
115
126
|
def find(id)
|
|
116
127
|
find_by("id" => id) || raise(RecordNotFound, "#{@model.table_name} #{id} not found")
|
|
@@ -118,7 +129,7 @@ module Edgeway
|
|
|
118
129
|
|
|
119
130
|
def build(attrs = {})
|
|
120
131
|
record = @model.new(attrs)
|
|
121
|
-
@
|
|
132
|
+
@eq_cols.each_with_index { |col, i| record.write_attribute(col, @eq_vals[i]) }
|
|
122
133
|
record
|
|
123
134
|
end
|
|
124
135
|
|
|
@@ -154,6 +165,7 @@ module Edgeway
|
|
|
154
165
|
@eager.each_with_index do |name, i|
|
|
155
166
|
many_flags << eager_many?(name)
|
|
156
167
|
other = Edgeway::Model::Base.model_named(name, @model.name)
|
|
168
|
+
check_same_database(name, other)
|
|
157
169
|
theirs = "eager_#{name}"
|
|
158
170
|
joins << " LEFT JOIN #{other.table_name} AS #{theirs} ON #{eager_on(name, other, theirs)}"
|
|
159
171
|
cols = cols + other.columns.map { |c| "#{theirs}.#{c} AS c#{i}_#{c}" }
|
|
@@ -168,7 +180,7 @@ module Edgeway
|
|
|
168
180
|
children = []
|
|
169
181
|
position = {}
|
|
170
182
|
seen = {}
|
|
171
|
-
|
|
183
|
+
@model.connection.select_all(sql, binds).each do |row|
|
|
172
184
|
id = row["p_id"]
|
|
173
185
|
i = position[id]
|
|
174
186
|
if i.nil?
|
|
@@ -205,6 +217,11 @@ module Edgeway
|
|
|
205
217
|
records
|
|
206
218
|
end
|
|
207
219
|
|
|
220
|
+
def check_same_database(name, other)
|
|
221
|
+
return if other.database == @model.database
|
|
222
|
+
raise ArgumentError, "eager_load :#{name} joins #{other.name} (database #{other.database}) to #{@model.name} (database #{@model.database}); a JOIN can't cross databases"
|
|
223
|
+
end
|
|
224
|
+
|
|
208
225
|
def eager_many?(name)
|
|
209
226
|
many = nil
|
|
210
227
|
@model.has_manys.each { |h| many = h if h.name == name }
|
|
@@ -223,9 +240,7 @@ module Edgeway
|
|
|
223
240
|
if many.nil?
|
|
224
241
|
one = nil
|
|
225
242
|
@model.belongs_to_list.each { |b| one = b if b.name == name }
|
|
226
|
-
#
|
|
227
|
-
# Spinel refuses this String as it is, or emits C that doesn't compile.
|
|
228
|
-
return "#{theirs}.#{other.column(one.primary_key.dup)} = #{mine}.#{@model.column(one.foreign_key.dup)}"
|
|
243
|
+
return "#{theirs}.#{other.column(one.primary_key)} = #{mine}.#{@model.column(one.foreign_key)}"
|
|
229
244
|
end
|
|
230
245
|
fk = many.foreign_key.nil? ? "#{Model.underscore(@model.name.split("::").last)}_id" : many.foreign_key
|
|
231
246
|
pk = many.primary_key.nil? ? "id" : many.primary_key
|
|
@@ -256,39 +271,74 @@ module Edgeway
|
|
|
256
271
|
"#{@model.column(col)} #{d.upcase}"
|
|
257
272
|
end
|
|
258
273
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
274
|
+
def add_fragment(sql, values, conds, binds)
|
|
275
|
+
conds << "(#{sql})"
|
|
276
|
+
values.each do |v|
|
|
277
|
+
raise ArgumentError, "an Array bind in a SQL fragment isn't expanded; use where(column: [...])" if v.is_a?(Array)
|
|
278
|
+
binds << Model.database_value(v)
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def add_in(column, type, values, conds, binds)
|
|
283
|
+
vals = values.map { |x| bind_value(type, x) }
|
|
284
|
+
rest = vals.compact
|
|
285
|
+
has_nil = rest.size < vals.size
|
|
286
|
+
if rest.empty?
|
|
287
|
+
conds << (has_nil ? "#{column} IS NULL" : "1 = 0")
|
|
288
|
+
return
|
|
289
|
+
end
|
|
290
|
+
term = "#{column} IN (#{rest.map { |_x| "?" }.join(", ")})"
|
|
291
|
+
conds << (has_nil ? "(#{term} OR #{column} IS NULL)" : term)
|
|
292
|
+
rest.each { |x| binds << x }
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def add_range(column, type, range, conds, binds)
|
|
296
|
+
lo = range.begin
|
|
297
|
+
hi = range.end
|
|
298
|
+
unless lo.nil?
|
|
299
|
+
conds << "#{column} >= ?"
|
|
300
|
+
binds << bind_value(type, lo)
|
|
301
|
+
end
|
|
302
|
+
unless hi.nil?
|
|
303
|
+
conds << "#{column} #{range.exclude_end? ? "<" : "<="} ?"
|
|
304
|
+
binds << bind_value(type, hi)
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def add_equality(column, type, value, conds, binds, eq_cols, eq_vals)
|
|
309
|
+
bound = bind_value(type, value)
|
|
310
|
+
if bound.nil?
|
|
311
|
+
conds << "#{column} IS NULL"
|
|
312
|
+
else
|
|
313
|
+
conds << "#{column} = ?"
|
|
314
|
+
binds << bound
|
|
271
315
|
end
|
|
272
|
-
|
|
316
|
+
eq_cols << column
|
|
317
|
+
eq_vals << bound
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# The value as it is bound. An integer outside 32 bits can't be held as
|
|
321
|
+
# one; it goes as its text, which SQLite compares as the number it is
|
|
322
|
+
# (an equality on it matches nothing: a find on it is a 404).
|
|
323
|
+
def bind_value(type, v)
|
|
324
|
+
v = Model.database_value(v) if v.is_a?(Time)
|
|
325
|
+
casted = Model.cast(type, v)
|
|
326
|
+
casted = v.to_s if casted.nil? && !v.nil?
|
|
327
|
+
Model.database_value(casted, type)
|
|
273
328
|
end
|
|
274
329
|
|
|
330
|
+
def where_clause = @conds.empty? ? "" : " WHERE " + @conds.join(" AND ")
|
|
331
|
+
|
|
275
332
|
def select(what, ordered)
|
|
276
|
-
|
|
277
|
-
where_sql, _marks = where_clause(conn)
|
|
278
|
-
sql = "SELECT #{what} FROM #{@model.table_name}#{where_sql}"
|
|
333
|
+
sql = "SELECT #{what} FROM #{@model.table_name}#{where_clause}"
|
|
279
334
|
sql += " ORDER BY #{@order.join(", ")}" if ordered && !@order.empty?
|
|
280
|
-
binds = @binds.compact
|
|
281
335
|
if @limit
|
|
282
|
-
|
|
283
|
-
sql += " LIMIT #{conn.placeholder(binds.size)}"
|
|
336
|
+
sql += " LIMIT #{@limit}"
|
|
284
337
|
elsif @offset
|
|
285
338
|
sql += " LIMIT -1"
|
|
286
339
|
end
|
|
287
|
-
if @offset
|
|
288
|
-
|
|
289
|
-
sql += " OFFSET #{conn.placeholder(binds.size)}"
|
|
290
|
-
end
|
|
291
|
-
[sql, binds]
|
|
340
|
+
sql += " OFFSET #{@offset}" if @offset
|
|
341
|
+
[sql, @binds]
|
|
292
342
|
end
|
|
293
343
|
end
|
|
294
344
|
end
|