diamond-orm 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/lib/diamond/api_catalog.rb +676 -0
- data/lib/diamond/ast.rb +397 -0
- data/lib/diamond/changeset.rb +36 -0
- data/lib/diamond/compiler/base.rb +17 -0
- data/lib/diamond/compiler/ddl.rb +94 -0
- data/lib/diamond/compiler/dml.rb +109 -0
- data/lib/diamond/compiler/dql.rb +390 -0
- data/lib/diamond/compiler/registry.rb +45 -0
- data/lib/diamond/cursor.rb +52 -0
- data/lib/diamond/domains/cte.rb +25 -0
- data/lib/diamond/domains/ddl.rb +11 -0
- data/lib/diamond/domains/dml.rb +106 -0
- data/lib/diamond/domains/dql.rb +395 -0
- data/lib/diamond/domains/dynamic_finders.rb +64 -0
- data/lib/diamond/dsl/default.rb +419 -0
- data/lib/diamond/engine.rb +226 -0
- data/lib/diamond/json_string.rb +13 -0
- data/lib/diamond/null_table.rb +10 -0
- data/lib/diamond/operator.rb +31 -0
- data/lib/diamond/operators/like.rb +142 -0
- data/lib/diamond/parser/proxy.rb +469 -0
- data/lib/diamond/parser/registry.rb +74 -0
- data/lib/diamond/parser.rb +571 -0
- data/lib/diamond/query_object.rb +463 -0
- data/lib/diamond/struct_factory.rb +262 -0
- data/lib/diamond/table.rb +32 -0
- data/lib/diamond/version.rb +3 -0
- data/lib/diamond.rb +436 -0
- data/sig/diamond.rbs +693 -0
- metadata +95 -0
data/lib/diamond/ast.rb
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
module Diamond
|
|
2
|
+
module AST
|
|
3
|
+
class Node
|
|
4
|
+
# so you can `&` / `|` any two conditions together.
|
|
5
|
+
def &(other)
|
|
6
|
+
AST::And.new(self, other)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def |(other)
|
|
10
|
+
AST::Or.new(self, other)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# `table` is nil for plain `age`, or a table name for qualified
|
|
15
|
+
# `tags.tag` refs in blocks over joins. qualified columns render with
|
|
16
|
+
# their own table and ignore the eager parent-prefix.
|
|
17
|
+
class Column < Node
|
|
18
|
+
attr_reader :name, :table
|
|
19
|
+
def initialize(name, table: nil); @name = name; @table = table; end
|
|
20
|
+
|
|
21
|
+
def ==(other)
|
|
22
|
+
AST::Equality.new(self, AST::Literal.new(other))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def !=(other)
|
|
26
|
+
AST::NotEqual.new(self, AST::Literal.new(other))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def >(other)
|
|
30
|
+
AST::GreaterThan.new(self, AST::Literal.new(other))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def <(other)
|
|
34
|
+
AST::LessThan.new(self, AST::Literal.new(other))
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class Literal < Node
|
|
39
|
+
attr_reader :value
|
|
40
|
+
def initialize(value); @value = value; end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class BinaryOp < Node
|
|
44
|
+
attr_reader :left, :right, :operator
|
|
45
|
+
def initialize(left, right, operator)
|
|
46
|
+
@left = left; @right = right; @operator = operator
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class Equality < BinaryOp
|
|
51
|
+
def initialize(left, right); super(left, right, :'='); end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class NotEqual < BinaryOp
|
|
55
|
+
def initialize(left, right); super(left, right, :'<>'); end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# `right` may hold expressions, not just literals.
|
|
59
|
+
class In < Node
|
|
60
|
+
attr_reader :left, :right
|
|
61
|
+
def initialize(left, right)
|
|
62
|
+
@left = left
|
|
63
|
+
@right = right
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
class NotIn < Node
|
|
68
|
+
attr_reader :left, :right
|
|
69
|
+
def initialize(left, right)
|
|
70
|
+
@left = left
|
|
71
|
+
@right = right
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Wraps a QueryObject as a SQL subquery: `(SELECT ...)`.
|
|
76
|
+
class Subquery < Node
|
|
77
|
+
attr_reader :query
|
|
78
|
+
def initialize(query); @query = query; end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
class GreaterThan < BinaryOp
|
|
82
|
+
def initialize(left, right); super(left, right, :'>'); end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class LessThan < BinaryOp
|
|
86
|
+
def initialize(left, right); super(left, right, :'<'); end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
class GreaterEqual < BinaryOp
|
|
90
|
+
def initialize(left, right); super(left, right, :'>='); end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
class LessEqual < BinaryOp
|
|
94
|
+
def initialize(left, right); super(left, right, :<=); end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# `!(cond)` and `not cond` are the same Prism shape. wraps anything;
|
|
98
|
+
# sqlite truthiness applies to non-boolean operands.
|
|
99
|
+
class Not < Node
|
|
100
|
+
attr_reader :condition
|
|
101
|
+
def initialize(condition); @condition = condition; end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
class Like < BinaryOp
|
|
105
|
+
def initialize(left, right); super(left, right, :LIKE); end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
class IsNull < Node
|
|
109
|
+
attr_reader :column
|
|
110
|
+
def initialize(column); @column = column; end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
class IsNotNull < Node
|
|
114
|
+
attr_reader :column
|
|
115
|
+
def initialize(column); @column = column; end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
class Between < Node
|
|
119
|
+
attr_reader :column, :low, :high
|
|
120
|
+
def initialize(column, low, high)
|
|
121
|
+
@column = column
|
|
122
|
+
@low = low
|
|
123
|
+
@high = high
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
class And < BinaryOp
|
|
128
|
+
def initialize(left, right); super(left, right, :AND); end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
class Or < BinaryOp
|
|
132
|
+
def initialize(left, right); super(left, right, :OR); end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
class Where < Node
|
|
136
|
+
attr_reader :condition
|
|
137
|
+
def initialize(condition); @condition = condition; end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
class Projection < Node
|
|
141
|
+
attr_reader :columns
|
|
142
|
+
def initialize(columns); @columns = columns; end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# marker: SELECT DISTINCT. carries nothing; presence is the flag.
|
|
146
|
+
class Distinct < Node
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# `on` maps LOCAL col to FOREIGN col, e.g. { user_id: :id }.
|
|
150
|
+
# `eager: true` marks the join for object-graph transform: the SQL gets
|
|
151
|
+
# column aliases (users.id AS users.id, posts.id AS posts.id, ...) and
|
|
152
|
+
# the compiler returns an Extralite::Transform spec that deduplicates
|
|
153
|
+
# and nests the rows.
|
|
154
|
+
class Join < Node
|
|
155
|
+
attr_reader :table_name, :type, :on, :eager, :single, :as
|
|
156
|
+
def initialize(table_name, type, on, eager: false, single: false, as: nil)
|
|
157
|
+
@table_name = table_name
|
|
158
|
+
@type = type
|
|
159
|
+
@on = on
|
|
160
|
+
@eager = eager
|
|
161
|
+
@single = single
|
|
162
|
+
@as = as
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
class With < Node
|
|
167
|
+
attr_reader :name, :query, :recursive
|
|
168
|
+
def initialize(name, query, recursive: false)
|
|
169
|
+
@name = name
|
|
170
|
+
@query = query
|
|
171
|
+
@recursive = recursive
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# `specs` is [[col, :asc|:desc], ...]. lands after WHERE in the sql.
|
|
176
|
+
class Order < Node
|
|
177
|
+
attr_reader :specs
|
|
178
|
+
def initialize(specs)
|
|
179
|
+
@specs = specs
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
class Limit < Node
|
|
184
|
+
attr_reader :value
|
|
185
|
+
def initialize(value)
|
|
186
|
+
@value = value
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
class Offset < Node
|
|
191
|
+
attr_reader :value
|
|
192
|
+
def initialize(value)
|
|
193
|
+
@value = value
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
class GroupBy < Node
|
|
198
|
+
attr_reader :columns
|
|
199
|
+
def initialize(columns)
|
|
200
|
+
@columns = columns # Array of Symbol column names
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
class Having < Node
|
|
205
|
+
attr_reader :condition
|
|
206
|
+
def initialize(condition)
|
|
207
|
+
@condition = condition
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# swap the FROM target (how you query a CTE).
|
|
212
|
+
class From < Node
|
|
213
|
+
attr_reader :name
|
|
214
|
+
def initialize(name); @name = name; end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
class Function < Node
|
|
218
|
+
attr_reader :name, :args
|
|
219
|
+
def initialize(name, args)
|
|
220
|
+
@name = name.to_s.upcase
|
|
221
|
+
@args = args # Array of AST::Column or AST::Literal
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
class WindowFunction < Node
|
|
226
|
+
attr_reader :func_name, :args, :partition_by, :order_by
|
|
227
|
+
|
|
228
|
+
def initialize(func_name, args, partition_by: [], order_by: [])
|
|
229
|
+
@func_name = func_name.to_s.upcase
|
|
230
|
+
@args = args
|
|
231
|
+
@partition_by = partition_by
|
|
232
|
+
@order_by = order_by
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
class Union < Node
|
|
237
|
+
attr_reader :left, :right, :operator
|
|
238
|
+
def initialize(left, right, operator = "UNION ALL")
|
|
239
|
+
@left = left
|
|
240
|
+
@right = right
|
|
241
|
+
@operator = operator
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# --- DDL Nodes ---
|
|
246
|
+
|
|
247
|
+
# column order is DDL order.
|
|
248
|
+
class DefineRelation < Node
|
|
249
|
+
attr_reader :name, :columns
|
|
250
|
+
def initialize(name, columns)
|
|
251
|
+
@name = name
|
|
252
|
+
@columns = columns
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# `type` is a ruby class. `options` knows :primary_key, :nullable, :default.
|
|
257
|
+
class ColumnDefinition < Node
|
|
258
|
+
attr_reader :name, :type, :options
|
|
259
|
+
def initialize(name, type, options = {})
|
|
260
|
+
@name = name
|
|
261
|
+
@type = type
|
|
262
|
+
@options = options
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# on_delete/on_update: :cascade, :set_null, :set_default, :restrict,
|
|
267
|
+
# :no_action. nil leaves the clause out.
|
|
268
|
+
class ForeignKey < Node
|
|
269
|
+
attr_reader :local_column, :ref_table, :ref_column, :on_delete, :on_update
|
|
270
|
+
def initialize(local_column, ref_table, ref_column = :id,
|
|
271
|
+
on_delete: nil, on_update: nil)
|
|
272
|
+
@local_column = local_column
|
|
273
|
+
@ref_table = ref_table
|
|
274
|
+
@ref_column = ref_column
|
|
275
|
+
@on_delete = on_delete
|
|
276
|
+
@on_update = on_update
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# lifted into the AST so define_relation does CREATE TABLE then
|
|
281
|
+
# CREATE INDEX in one go.
|
|
282
|
+
class IndexDefinition < Node
|
|
283
|
+
attr_reader :name, :columns, :unique
|
|
284
|
+
def initialize(name, columns, unique: false)
|
|
285
|
+
@name = name
|
|
286
|
+
@columns = columns
|
|
287
|
+
@unique = unique
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# --- DML Nodes ---
|
|
292
|
+
|
|
293
|
+
# `data`: Hash mapping column name (Symbol) to value.
|
|
294
|
+
class Insert < Node
|
|
295
|
+
attr_reader :data
|
|
296
|
+
def initialize(data)
|
|
297
|
+
@data = data
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# `data`: Hash mapping column name (Symbol) to value.
|
|
302
|
+
class Update < Node
|
|
303
|
+
attr_reader :data
|
|
304
|
+
def initialize(data)
|
|
305
|
+
@data = data
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# rows come from the chain's WHERE nodes.
|
|
310
|
+
class Delete < Node
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# indented tree dump for staring at what a block became. one line
|
|
314
|
+
# per node, children indented two spaces. QueryObject#ast_tree maps
|
|
315
|
+
# this over the chain.
|
|
316
|
+
def self.dump(node, indent = 0)
|
|
317
|
+
pad = ' ' * indent
|
|
318
|
+
label = case node
|
|
319
|
+
when Column
|
|
320
|
+
node.table ? "Column(#{node.table}.#{node.name})" : "Column(#{node.name})"
|
|
321
|
+
when Literal
|
|
322
|
+
"Literal(#{node.value.inspect})"
|
|
323
|
+
when BinaryOp
|
|
324
|
+
node.class.name.split('::').last
|
|
325
|
+
when In, NotIn
|
|
326
|
+
"#{node.class.name.split('::').last}(#{node.right.size} vals)"
|
|
327
|
+
when IsNull, IsNotNull
|
|
328
|
+
node.class.name.split('::').last
|
|
329
|
+
when Between
|
|
330
|
+
'Between'
|
|
331
|
+
when Not
|
|
332
|
+
'Not'
|
|
333
|
+
when Subquery
|
|
334
|
+
'Subquery'
|
|
335
|
+
when Function
|
|
336
|
+
"#{node.name}(#{node.args.size} args)"
|
|
337
|
+
when WindowFunction
|
|
338
|
+
"#{node.func_name} OVER"
|
|
339
|
+
when Where
|
|
340
|
+
'Where'
|
|
341
|
+
when Projection
|
|
342
|
+
"Projection(#{node.columns.size} cols)"
|
|
343
|
+
when Join
|
|
344
|
+
"Join(#{node.table_name}, #{node.type}#{node.eager ? ', eager' : ''})"
|
|
345
|
+
when Order
|
|
346
|
+
labels = node.specs.map do |c, d|
|
|
347
|
+
name = c.is_a?(Column) && c.table ? "#{c.table}.#{c.name}" : c.to_s
|
|
348
|
+
"#{name} #{d}"
|
|
349
|
+
end
|
|
350
|
+
"Order(#{labels.join(', ')})"
|
|
351
|
+
when Limit
|
|
352
|
+
"Limit(#{node.value})"
|
|
353
|
+
when Offset
|
|
354
|
+
"Offset(#{node.value})"
|
|
355
|
+
when GroupBy
|
|
356
|
+
"GroupBy(#{node.columns.join(', ')})"
|
|
357
|
+
when Having
|
|
358
|
+
'Having'
|
|
359
|
+
when From
|
|
360
|
+
"From(#{node.name})"
|
|
361
|
+
when With
|
|
362
|
+
"With(#{node.name}#{node.recursive ? ', recursive' : ''})"
|
|
363
|
+
when Union
|
|
364
|
+
"Union(#{node.operator})"
|
|
365
|
+
else
|
|
366
|
+
node.class.name.split('::').last
|
|
367
|
+
end
|
|
368
|
+
lines = ["#{pad}#{label}"]
|
|
369
|
+
kids = case node
|
|
370
|
+
when BinaryOp then [node.left, node.right]
|
|
371
|
+
when In, NotIn then [node.left] + (node.right.is_a?(Array) ? node.right : [node.right])
|
|
372
|
+
when IsNull, IsNotNull then [node.column]
|
|
373
|
+
when Between then [node.column, node.low, node.high]
|
|
374
|
+
when Not then [node.condition]
|
|
375
|
+
when Function then node.args
|
|
376
|
+
when Where, Having then [node.condition]
|
|
377
|
+
when Projection then node.columns
|
|
378
|
+
when Union then [node.left, node.right]
|
|
379
|
+
else []
|
|
380
|
+
end
|
|
381
|
+
kids.each do |k|
|
|
382
|
+
lines << (k.is_a?(Node) ? dump(k, indent + 1) : "#{pad} #{k.inspect}")
|
|
383
|
+
end
|
|
384
|
+
# With/Subquery hold whole queries — a Union node or a QueryObject
|
|
385
|
+
# carrying a chain. dump whichever it is.
|
|
386
|
+
if node.is_a?(With) || node.is_a?(Subquery)
|
|
387
|
+
q = node.query
|
|
388
|
+
if q.is_a?(Node)
|
|
389
|
+
lines << dump(q, indent + 1)
|
|
390
|
+
else
|
|
391
|
+
q.ast.each { |n| lines << dump(n, indent + 1) }
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
lines.join("\n")
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Diamond
|
|
2
|
+
# Thin wrapper around Extralite::Changeset. Provides .apply(target_db),
|
|
3
|
+
# .invert, .to_blob, .load(blob) under a Diamond-y namespace.
|
|
4
|
+
#
|
|
5
|
+
# Used by Diamond.track_changes to keep callers from having to
|
|
6
|
+
# know that the underlying type is Extralite-specific.
|
|
7
|
+
class Changeset
|
|
8
|
+
def initialize(raw_changeset)
|
|
9
|
+
@raw = raw_changeset
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Replay the changeset on a target database (another Extralite db,
|
|
13
|
+
# a Diamond Engine, or your own test DB).
|
|
14
|
+
def apply(target)
|
|
15
|
+
target_db = target.is_a?(Diamond::Engine) ? target.db : target
|
|
16
|
+
@raw.apply(target_db)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Returns a new Changeset that, when applied, undoes this one.
|
|
20
|
+
def invert
|
|
21
|
+
Diamond::Changeset.new(@raw.invert)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Serialize for network transfer.
|
|
25
|
+
def to_blob
|
|
26
|
+
@raw.to_blob
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Build a Changeset from a serialized blob.
|
|
30
|
+
def self.load(blob)
|
|
31
|
+
cs = Extralite::Changeset.new
|
|
32
|
+
cs.load(blob)
|
|
33
|
+
Diamond::Changeset.new(cs)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require_relative 'dql'
|
|
2
|
+
require_relative 'dml'
|
|
3
|
+
require_relative 'ddl'
|
|
4
|
+
require_relative 'registry'
|
|
5
|
+
|
|
6
|
+
module Diamond
|
|
7
|
+
module Compiler
|
|
8
|
+
module Base
|
|
9
|
+
# Returns [sql, params, transform_or_nil]. The transform is non-nil
|
|
10
|
+
# only for eager-loaded queries; non-eager callers can ignore it.
|
|
11
|
+
def self.compile(table, ast, params = nil)
|
|
12
|
+
params ||= []
|
|
13
|
+
DQL.compile(table, ast, params)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
module Diamond
|
|
2
|
+
module Compiler
|
|
3
|
+
module DDL
|
|
4
|
+
TYPE_MAP = {
|
|
5
|
+
Integer => 'INTEGER',
|
|
6
|
+
String => 'TEXT',
|
|
7
|
+
Float => 'REAL',
|
|
8
|
+
TrueClass => 'INTEGER',
|
|
9
|
+
FalseClass => 'INTEGER'
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
def self.compile(node)
|
|
13
|
+
raise ArgumentError, "Expected AST::DefineRelation, got #{node.class}" unless node.is_a?(AST::DefineRelation)
|
|
14
|
+
Diamond.validate_ident!(node.name, "table name")
|
|
15
|
+
|
|
16
|
+
column_clauses = node.columns.select { |c| c.is_a?(AST::ColumnDefinition) }.map { |c| render_column(c) }
|
|
17
|
+
fk_clauses = node.columns.select { |c| c.is_a?(AST::ForeignKey) }.map { |c| render_foreign_key(c) }
|
|
18
|
+
|
|
19
|
+
body = (column_clauses + fk_clauses).join(', ')
|
|
20
|
+
sql = "CREATE TABLE IF NOT EXISTS #{node.name} (#{body})"
|
|
21
|
+
[sql, []]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.render_column(node)
|
|
25
|
+
Diamond.validate_ident!(node.name, "column name")
|
|
26
|
+
sql_type = TYPE_MAP[node.type] or raise ArgumentError, "Unsupported column type: #{node.type}"
|
|
27
|
+
parts = [node.name.to_s, sql_type]
|
|
28
|
+
|
|
29
|
+
if node.options[:primary_key]
|
|
30
|
+
parts << 'PRIMARY KEY'
|
|
31
|
+
elsif node.options[:nullable] == false
|
|
32
|
+
parts << 'NOT NULL'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if node.options.key?(:default)
|
|
36
|
+
parts << "DEFAULT #{render_default!(node.options[:default])}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
parts.join(' ')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# DDL cannot bind parameters, so DEFAULT literals go through a
|
|
43
|
+
# strict allowlist — numerics, booleans (as 1/0), and strings
|
|
44
|
+
# with single-quote escaping. Anything else raises instead of
|
|
45
|
+
# interpolating raw input into SQL.
|
|
46
|
+
def self.render_default!(value)
|
|
47
|
+
case value
|
|
48
|
+
when Numeric then value.to_s
|
|
49
|
+
when true then '1'
|
|
50
|
+
when false then '0'
|
|
51
|
+
when String then "'#{value.gsub("'", "''")}'"
|
|
52
|
+
when nil then 'NULL'
|
|
53
|
+
else raise ArgumentError, "Unsupported DEFAULT value: #{value.inspect}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# ALTER TABLE ... ADD COLUMN for safe migrations. Only plain
|
|
58
|
+
# column definitions allowed — no PK/FK/index nodes (SQLite
|
|
59
|
+
# rejects most of those via ADD COLUMN anyway).
|
|
60
|
+
def self.compile_add_column(table_name, coldef)
|
|
61
|
+
raise ArgumentError, "Expected AST::ColumnDefinition, got #{coldef.class}" unless coldef.is_a?(AST::ColumnDefinition)
|
|
62
|
+
Diamond.validate_ident!(table_name, "table name")
|
|
63
|
+
if coldef.options[:primary_key]
|
|
64
|
+
raise ArgumentError, "Cannot ADD COLUMN #{coldef.name} as PRIMARY KEY (SQLite restriction)"
|
|
65
|
+
end
|
|
66
|
+
sql = "ALTER TABLE #{table_name} ADD COLUMN #{render_column(coldef)}"
|
|
67
|
+
[sql, []]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.render_foreign_key(node)
|
|
71
|
+
Diamond.validate_ident!(node.local_column, "foreign key column")
|
|
72
|
+
Diamond.validate_ident!(node.ref_table, "referenced table")
|
|
73
|
+
Diamond.validate_ident!(node.ref_column, "referenced column")
|
|
74
|
+
sql = "FOREIGN KEY (#{node.local_column}) REFERENCES #{node.ref_table}(#{node.ref_column})"
|
|
75
|
+
sql += " ON DELETE #{format_action(node.on_delete)}" if node.on_delete
|
|
76
|
+
sql += " ON UPDATE #{format_action(node.on_update)}" if node.on_update
|
|
77
|
+
sql
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.format_action(sym)
|
|
81
|
+
sym.to_s.upcase.tr('_', ' ')
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.compile_index(index_node, table_name)
|
|
85
|
+
Diamond.validate_ident!(index_node.name, "index name")
|
|
86
|
+
Diamond.validate_ident!(table_name, "table name")
|
|
87
|
+
index_node.columns.each { |c| Diamond.validate_ident!(c, "indexed column") }
|
|
88
|
+
unique_kw = index_node.unique ? 'UNIQUE ' : ''
|
|
89
|
+
sql = "CREATE #{unique_kw}INDEX IF NOT EXISTS #{index_node.name} ON #{table_name}(#{index_node.columns.join(', ')})"
|
|
90
|
+
[sql, []]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
require_relative 'dql'
|
|
2
|
+
|
|
3
|
+
module Diamond
|
|
4
|
+
module Compiler
|
|
5
|
+
module DML
|
|
6
|
+
def self.compile_insert(table, insert_node, returning: [])
|
|
7
|
+
cols = insert_node.data.keys
|
|
8
|
+
placeholders = (['?'] * cols.size).join(', ')
|
|
9
|
+
sql = +"INSERT INTO #{table.name} (#{cols.join(', ')}) VALUES (#{placeholders})"
|
|
10
|
+
params = insert_node.data.values
|
|
11
|
+
|
|
12
|
+
if returning && !returning.empty?
|
|
13
|
+
sql << " RETURNING " << returning.map { |c| c.to_s }.join(', ')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
db = Diamond.engine.db
|
|
17
|
+
stmt = db.prepare(sql)
|
|
18
|
+
begin
|
|
19
|
+
stmt.bind(*params)
|
|
20
|
+
if returning && !returning.empty?
|
|
21
|
+
# collect rows from the RETURNING clause
|
|
22
|
+
rows = []
|
|
23
|
+
stmt.each { |row| rows << row }
|
|
24
|
+
rows
|
|
25
|
+
else
|
|
26
|
+
stmt.to_a # forces execution
|
|
27
|
+
[]
|
|
28
|
+
end
|
|
29
|
+
ensure
|
|
30
|
+
begin
|
|
31
|
+
stmt.close unless stmt.closed?
|
|
32
|
+
rescue StandardError
|
|
33
|
+
# ensure must not raise
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
returning && !returning.empty? ? rows : db.last_insert_rowid
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.compile_update(table, hash, where_nodes, returning: [])
|
|
40
|
+
params = []
|
|
41
|
+
set_clause = hash.keys.map { |k| "#{k} = ?" }.join(', ')
|
|
42
|
+
params.concat(hash.values)
|
|
43
|
+
|
|
44
|
+
sql = +"UPDATE #{table.name} SET #{set_clause}"
|
|
45
|
+
unless where_nodes.empty?
|
|
46
|
+
conditions = where_nodes.map { |w| DQL.translate_node(w.condition, params) }
|
|
47
|
+
sql << " WHERE " << conditions.join(' AND ')
|
|
48
|
+
end
|
|
49
|
+
if returning && !returning.empty?
|
|
50
|
+
sql << " RETURNING " << returning.map { |c| c.to_s }.join(', ')
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
db = Diamond.engine.db
|
|
54
|
+
stmt = db.prepare(sql)
|
|
55
|
+
rows = nil
|
|
56
|
+
begin
|
|
57
|
+
stmt.bind(*params)
|
|
58
|
+
if returning && !returning.empty?
|
|
59
|
+
rows = []
|
|
60
|
+
stmt.each { |row| rows << row }
|
|
61
|
+
else
|
|
62
|
+
stmt.to_a
|
|
63
|
+
end
|
|
64
|
+
ensure
|
|
65
|
+
begin
|
|
66
|
+
stmt.close unless stmt.closed?
|
|
67
|
+
rescue StandardError
|
|
68
|
+
# ensure must not raise
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
returning && !returning.empty? ? rows : db.changes
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.compile_delete(table, where_nodes, returning: [])
|
|
76
|
+
params = []
|
|
77
|
+
sql = +"DELETE FROM #{table.name}"
|
|
78
|
+
unless where_nodes.empty?
|
|
79
|
+
conditions = where_nodes.map { |w| DQL.translate_node(w.condition, params) }
|
|
80
|
+
sql << " WHERE " << conditions.join(' AND ')
|
|
81
|
+
end
|
|
82
|
+
if returning && !returning.empty?
|
|
83
|
+
sql << " RETURNING " << returning.map { |c| c.to_s }.join(', ')
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
db = Diamond.engine.db
|
|
87
|
+
stmt = db.prepare(sql)
|
|
88
|
+
rows = nil
|
|
89
|
+
begin
|
|
90
|
+
stmt.bind(*params)
|
|
91
|
+
if returning && !returning.empty?
|
|
92
|
+
rows = []
|
|
93
|
+
stmt.each { |row| rows << row }
|
|
94
|
+
else
|
|
95
|
+
stmt.to_a
|
|
96
|
+
end
|
|
97
|
+
ensure
|
|
98
|
+
begin
|
|
99
|
+
stmt.close unless stmt.closed?
|
|
100
|
+
rescue StandardError
|
|
101
|
+
# ensure must not raise
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
returning && !returning.empty? ? rows : db.changes
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|