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
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
require_relative 'compiler/dql'
|
|
2
|
+
require 'json'
|
|
3
|
+
|
|
4
|
+
module Diamond
|
|
5
|
+
# JSON writer mixed into every Diamond struct class. Serializes
|
|
6
|
+
# straight from positional values — `{"id":1,"name":"x"}` — without
|
|
7
|
+
# the intermediate Hash that `to_h` allocates. Makes
|
|
8
|
+
# `JSON.generate(array_of_structs)` correct (stdlib has no Struct
|
|
9
|
+
# support and renders `"#<struct …>"` garbage) and fast.
|
|
10
|
+
module StructJSON
|
|
11
|
+
def to_json(*)
|
|
12
|
+
out = +'{'
|
|
13
|
+
first = true
|
|
14
|
+
each_pair do |member, value|
|
|
15
|
+
out << ',' unless first
|
|
16
|
+
first = false
|
|
17
|
+
out << '"' << member.to_s << '":'
|
|
18
|
+
out << JSON.generate(value)
|
|
19
|
+
end
|
|
20
|
+
out << '}'
|
|
21
|
+
out
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
module StructFactory
|
|
26
|
+
# struct classes pile up per projection shape. cached per-Ractor on
|
|
27
|
+
# Ractor.current's local storage so each worker warms its own cache
|
|
28
|
+
# without tripping isolation errors.
|
|
29
|
+
CACHES_KEY = Diamond::RACTOR_KEYS[:struct_caches]
|
|
30
|
+
|
|
31
|
+
def self.caches
|
|
32
|
+
Ractor.current[CACHES_KEY] ||= Hash.new { |h, k| h[k] = {} }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.cache
|
|
36
|
+
caches[:structs]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.clear_caches!
|
|
40
|
+
Ractor.current[CACHES_KEY] = Hash.new { |h, k| h[k] = {} }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.create(table, row_hash, projection_nodes = nil)
|
|
44
|
+
members = resolve_members(projection_nodes, table)
|
|
45
|
+
|
|
46
|
+
member_names = members.map(&:first)
|
|
47
|
+
klass = struct_class_for(table, member_names, projection_nodes)
|
|
48
|
+
|
|
49
|
+
values = members.map { |member, sql_name| row_hash[sql_name] }
|
|
50
|
+
klass.new(*values).freeze
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Build a frozen struct from a RETURNING result row (one of the
|
|
54
|
+
# columns the caller asked for, in the order asked). Reuses the
|
|
55
|
+
# existing struct cache keyed by member-name list.
|
|
56
|
+
def self.create_from_returning(table, row, columns)
|
|
57
|
+
member_names = columns.map(&:to_sym)
|
|
58
|
+
cache_key = "#{table.name}\0#{member_names.join("\0")}"
|
|
59
|
+
klass = cache[cache_key] ||= Struct.new(*member_names) do
|
|
60
|
+
include Diamond::StructJSON
|
|
61
|
+
def save; raise Diamond::InertObjectError, "Data is inert! Use the Table proxy to update."; end
|
|
62
|
+
end
|
|
63
|
+
values = columns.map { |c| row[c.to_sym] }
|
|
64
|
+
klass.new(*values).freeze
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Build a frozen struct from an Extralite::Transform result row. The
|
|
68
|
+
# row shape is a Hash keyed by the transform spec's keys; for parent
|
|
69
|
+
# columns the key is the bare column name (e.g. "id"), for eagerly-
|
|
70
|
+
# loaded children the key is the table name and the value is an Array
|
|
71
|
+
# of nested Hashes (each becoming a child struct).
|
|
72
|
+
#
|
|
73
|
+
# Post-processes the Extralite output:
|
|
74
|
+
# - strips the "table." prefix from nested column names
|
|
75
|
+
# - drops null sentinel rows LEFT JOIN leaves for parents without kids
|
|
76
|
+
# - deduplicates children by primary key (Extralite only dedupes
|
|
77
|
+
# within a single parent row's join group, not across the join)
|
|
78
|
+
def self.create_eager(parent_table, row)
|
|
79
|
+
members = []
|
|
80
|
+
table_sym = parent_table.is_a?(Symbol) ? parent_table : parent_table.name
|
|
81
|
+
|
|
82
|
+
row.each do |key, value|
|
|
83
|
+
if value.is_a?(Array)
|
|
84
|
+
# collection (to-many eager join)
|
|
85
|
+
member_name = key.to_sym
|
|
86
|
+
# extract actual child table name from column aliases (table.col)
|
|
87
|
+
sample = value.first
|
|
88
|
+
child_table_sym = if sample.is_a?(Hash) && sample.keys.first.to_s.include?('.')
|
|
89
|
+
sample.keys.first.to_s.split('.', 2).first.to_sym
|
|
90
|
+
else
|
|
91
|
+
member_name
|
|
92
|
+
end
|
|
93
|
+
child_schema = Diamond.engine.schema_cache[child_table_sym]
|
|
94
|
+
child_pk = child_schema[:primary_key]
|
|
95
|
+
|
|
96
|
+
# strip table. prefix from each child row's keys
|
|
97
|
+
stripped = value.map do |child_row|
|
|
98
|
+
child_row.each_with_object({}) do |(k, v), h|
|
|
99
|
+
short = k.to_s.sub(/^#{Regexp.escape(child_table_sym.to_s)}\./, '').to_sym
|
|
100
|
+
h[short] = v
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# drop null sentinels (Extralite leaves these for parents without
|
|
105
|
+
# children after a LEFT JOIN)
|
|
106
|
+
stripped.reject! { |child_row| child_row.values.all?(&:nil?) }
|
|
107
|
+
|
|
108
|
+
# dedupe by primary key
|
|
109
|
+
if child_pk
|
|
110
|
+
seen = {}
|
|
111
|
+
stripped = stripped.reject do |r|
|
|
112
|
+
pk_val = r[child_pk]
|
|
113
|
+
if seen[pk_val]
|
|
114
|
+
true
|
|
115
|
+
else
|
|
116
|
+
seen[pk_val] = true
|
|
117
|
+
false
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
child_structs = stripped.map { |child_row| create_eager(child_table_sym, child_row) }
|
|
123
|
+
members << [member_name, child_structs]
|
|
124
|
+
elsif value.is_a?(Hash)
|
|
125
|
+
# single object (to-one eager join)
|
|
126
|
+
member_name = key.to_sym
|
|
127
|
+
# the key in the transform output is the member name (may be aliased).
|
|
128
|
+
# the actual child table name is encoded in the column aliases (table.col).
|
|
129
|
+
# extract it from any key to find the schema for stripping prefixes.
|
|
130
|
+
sample_key = value.keys.first
|
|
131
|
+
child_table_sym = if sample_key.to_s.include?('.')
|
|
132
|
+
sample_key.to_s.split('.', 2).first.to_sym
|
|
133
|
+
else
|
|
134
|
+
# no dotted key — fall back to member name as table name
|
|
135
|
+
member_name
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# strip table. prefix from keys
|
|
139
|
+
stripped = value.each_with_object({}) do |(k, v), h|
|
|
140
|
+
short = k.to_s.sub(/^#{Regexp.escape(child_table_sym.to_s)}\./, '').to_sym
|
|
141
|
+
h[short] = v
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# drop null sentinel (parent without child) -> nil
|
|
145
|
+
if stripped.values.all?(&:nil?)
|
|
146
|
+
members << [member_name, nil]
|
|
147
|
+
else
|
|
148
|
+
child_struct = create_eager(child_table_sym, stripped)
|
|
149
|
+
members << [member_name, child_struct]
|
|
150
|
+
end
|
|
151
|
+
else
|
|
152
|
+
members << [key.to_sym, value]
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
member_names = members.map(&:first)
|
|
157
|
+
cache_key = "#{table_sym}\0#{member_names.join("\0")}"
|
|
158
|
+
klass = cache[cache_key] ||= Struct.new(*member_names) do
|
|
159
|
+
include Diamond::StructJSON
|
|
160
|
+
def save; raise Diamond::InertObjectError, "Data is inert! Use the Table proxy to update."; end
|
|
161
|
+
end
|
|
162
|
+
klass.new(*members.map(&:last)).freeze
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# array rows land positionally, no hash lookups. select order ==
|
|
166
|
+
# member order, `*` included. takes the first members.size values:
|
|
167
|
+
# joined `SELECT *` yields trailing columns of other tables that
|
|
168
|
+
# the hash path ignores by key — slicing keeps both paths identical.
|
|
169
|
+
def self.create_from_array(table, row_array, projection_nodes = nil)
|
|
170
|
+
build(factory_for(table, projection_nodes), row_array)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Bulk path for materialize: resolves members + struct class ONCE
|
|
174
|
+
# per query instead of per row (the dominant cost in the hot loop
|
|
175
|
+
# per bench/rails/breakdown.rb: ~59% of collection time).
|
|
176
|
+
def self.create_many(table, rows, projection_nodes = nil)
|
|
177
|
+
factory = factory_for(table, projection_nodes)
|
|
178
|
+
rows.map { |row| build(factory, row) }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Pre-resolved [klass, member_count] for streaming consumers
|
|
182
|
+
# (Cursor) that cannot collect rows up front.
|
|
183
|
+
def self.factory_for(table, projection_nodes = nil)
|
|
184
|
+
members = resolve_members(projection_nodes, table)
|
|
185
|
+
member_names = members.map(&:first)
|
|
186
|
+
[struct_class_for(table, member_names, projection_nodes), member_names.size]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def self.build(factory, row_array)
|
|
190
|
+
klass, count = factory
|
|
191
|
+
klass.new(*(row_array.size == count ? row_array : row_array.first(count))).freeze
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def self.struct_class_for(table, member_names, projection_nodes)
|
|
195
|
+
# NUL can't appear in identifiers, so [a_b, c] and [a, b_c] stop colliding.
|
|
196
|
+
cache_key = projection_nodes ? "#{table.name}\0#{member_names.join("\0")}" : table.name.to_s
|
|
197
|
+
|
|
198
|
+
unless cache[cache_key]
|
|
199
|
+
cache[cache_key] = Struct.new(*member_names) do
|
|
200
|
+
include Diamond::StructJSON
|
|
201
|
+
def save; raise Diamond::InertObjectError, "Data is inert! Use the Table proxy to update."; end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
cache[cache_key]
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def self.resolve_members(projection_nodes, table)
|
|
209
|
+
if projection_nodes.nil? || projection_nodes.empty?
|
|
210
|
+
return table.schema[:columns].map { |c| [c, c] }
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
projection_nodes.map do |node|
|
|
214
|
+
sql_name = compile_node(node)
|
|
215
|
+
member_name = member_name_for(node)
|
|
216
|
+
[member_name, sql_name]
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def self.compile_node(node)
|
|
221
|
+
# Extralite returns symbol keys for both columns and function results
|
|
222
|
+
# (e.g. { :id => 1, :"COUNT(id)" => 4 }). We mirror that here so
|
|
223
|
+
# `row_hash[sql_name]` hits the right key.
|
|
224
|
+
case node
|
|
225
|
+
when Diamond::AST::Column
|
|
226
|
+
node.name.to_sym
|
|
227
|
+
when Diamond::AST::Function
|
|
228
|
+
Diamond::Compiler::DQL.translate_node(node, []).to_sym
|
|
229
|
+
when Diamond::AST::WindowFunction
|
|
230
|
+
Diamond::Compiler::DQL.translate_node(node, []).to_sym
|
|
231
|
+
else
|
|
232
|
+
Diamond::Compiler::DQL.translate_node(node, []).to_sym
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def self.member_name_for(node)
|
|
237
|
+
case node
|
|
238
|
+
when Diamond::AST::Column
|
|
239
|
+
node.name
|
|
240
|
+
when Diamond::AST::Function
|
|
241
|
+
first_arg = node.args.first
|
|
242
|
+
suffix = case first_arg
|
|
243
|
+
when Diamond::AST::Column then first_arg.name
|
|
244
|
+
when Diamond::AST::Literal
|
|
245
|
+
val = first_arg.value
|
|
246
|
+
val.is_a?(String) ? val : (val.nil? ? 'nil' : val.to_s)
|
|
247
|
+
else
|
|
248
|
+
'all'
|
|
249
|
+
end
|
|
250
|
+
:"#{node.name.downcase}_#{suffix}"
|
|
251
|
+
when Diamond::AST::WindowFunction
|
|
252
|
+
parts = []
|
|
253
|
+
parts.concat(node.partition_by) unless node.partition_by.empty?
|
|
254
|
+
parts.concat(node.order_by) unless node.order_by.empty?
|
|
255
|
+
suffix = parts.empty? ? 'all' : parts.join('_')
|
|
256
|
+
:"#{node.func_name.downcase}_#{suffix}"
|
|
257
|
+
else
|
|
258
|
+
raise "Unknown projection node: #{node.class}"
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require_relative 'ast'
|
|
2
|
+
require_relative 'query_object'
|
|
3
|
+
|
|
4
|
+
module Diamond
|
|
5
|
+
class Table
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
attr_reader :name, :schema
|
|
9
|
+
|
|
10
|
+
def initialize(name)
|
|
11
|
+
@name = name
|
|
12
|
+
@schema = Diamond.engine.schema_cache[name]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# column names in DDL order.
|
|
16
|
+
def columns
|
|
17
|
+
@schema[:columns]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# primary key column, or nil.
|
|
21
|
+
def primary_key
|
|
22
|
+
@schema[:primary_key]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Sequel-style sugar: Users[1] → find(1).first. Returns the frozen
|
|
26
|
+
# struct, or nil when no row matches (Hash-like miss semantics —
|
|
27
|
+
# use find! when you want RecordNotFound instead).
|
|
28
|
+
def [](id)
|
|
29
|
+
find(id).first
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|