pglite 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE.txt +23 -0
- data/README.md +46 -0
- data/ext/Cargo.lock +3336 -0
- data/ext/Cargo.toml +24 -0
- data/ext/extconf.rb +6 -0
- data/ext/src/lib.rs +487 -0
- data/lib/pglite/version.rb +5 -0
- data/lib/pglite.rb +13 -0
- metadata +139 -0
data/ext/Cargo.toml
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "pglite_rb_ext"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
|
|
6
|
+
[lib]
|
|
7
|
+
name = "pglite_rb_ext"
|
|
8
|
+
crate-type = ["cdylib"]
|
|
9
|
+
|
|
10
|
+
[dependencies]
|
|
11
|
+
magnus = "0.8"
|
|
12
|
+
pglite-oxide = "0.1.0"
|
|
13
|
+
tokio = { version = "1.0", features = ["rt", "rt-multi-thread", "macros"] }
|
|
14
|
+
tokio-postgres = "0.7"
|
|
15
|
+
bytes = "1.0"
|
|
16
|
+
hex = "0.4"
|
|
17
|
+
anyhow = "1.0"
|
|
18
|
+
|
|
19
|
+
[dependencies.serde]
|
|
20
|
+
version = "1.0"
|
|
21
|
+
features = ["derive"]
|
|
22
|
+
|
|
23
|
+
[dependencies.serde_json]
|
|
24
|
+
version = "1.0"
|
data/ext/extconf.rb
ADDED
data/ext/src/lib.rs
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
use magnus::{
|
|
2
|
+
function, method,
|
|
3
|
+
prelude::*,
|
|
4
|
+
Error, RString, Ruby, Value, RArray,
|
|
5
|
+
};
|
|
6
|
+
use std::sync::OnceLock;
|
|
7
|
+
|
|
8
|
+
// Global runtime for handling pglite-oxide operations
|
|
9
|
+
static RUNTIME: OnceLock<()> = OnceLock::new();
|
|
10
|
+
|
|
11
|
+
// Initialize pglite-oxide runtime once
|
|
12
|
+
fn ensure_runtime() {
|
|
13
|
+
RUNTIME.get_or_init(|| {
|
|
14
|
+
// Initialize any global state if needed
|
|
15
|
+
()
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Intermediate struct for parsing that doesn't contain Ruby values
|
|
20
|
+
#[derive(Debug)]
|
|
21
|
+
struct ParsedResult {
|
|
22
|
+
command: String,
|
|
23
|
+
columns: Vec<String>,
|
|
24
|
+
rows: Vec<Vec<ParsedValue>>,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#[derive(Debug, Clone)]
|
|
28
|
+
enum ParsedValue {
|
|
29
|
+
Null,
|
|
30
|
+
Bool(bool),
|
|
31
|
+
Integer(i64),
|
|
32
|
+
Float(f64),
|
|
33
|
+
Text(String),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Result struct that will be exposed to Ruby
|
|
37
|
+
#[derive(Clone)]
|
|
38
|
+
#[magnus::wrap(class = "PGlite::Oxide::Result")]
|
|
39
|
+
struct QueryResult {
|
|
40
|
+
command: String,
|
|
41
|
+
columns: Vec<String>,
|
|
42
|
+
row_count: usize,
|
|
43
|
+
// Store raw data that can be converted to Ruby values on demand
|
|
44
|
+
raw_rows: Vec<Vec<ParsedValue>>,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
impl QueryResult {
|
|
48
|
+
fn new(parsed: ParsedResult) -> Self {
|
|
49
|
+
let row_count = parsed.rows.len();
|
|
50
|
+
Self {
|
|
51
|
+
command: parsed.command,
|
|
52
|
+
columns: parsed.columns,
|
|
53
|
+
row_count,
|
|
54
|
+
raw_rows: parsed.rows,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Ruby method: result.command
|
|
59
|
+
fn command(&self) -> String {
|
|
60
|
+
self.command.clone()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Ruby method: result.columns
|
|
64
|
+
fn columns(&self) -> Result<RArray, Error> {
|
|
65
|
+
let ruby = Ruby::get().map_err(|e| Error::new(Ruby::get().unwrap().exception_runtime_error(), format!("Ruby context unavailable: {}", e)))?;
|
|
66
|
+
let array = ruby.ary_new_capa(self.columns.len());
|
|
67
|
+
for col in &self.columns {
|
|
68
|
+
array.push(ruby.str_new(col))?;
|
|
69
|
+
}
|
|
70
|
+
Ok(array)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Ruby method: result.rows
|
|
74
|
+
fn rows(&self) -> Result<RArray, Error> {
|
|
75
|
+
let ruby = Ruby::get().map_err(|e| Error::new(Ruby::get().unwrap().exception_runtime_error(), format!("Ruby context unavailable: {}", e)))?;
|
|
76
|
+
let rows_array = ruby.ary_new_capa(self.raw_rows.len());
|
|
77
|
+
|
|
78
|
+
for row in &self.raw_rows {
|
|
79
|
+
let row_array = ruby.ary_new_capa(row.len());
|
|
80
|
+
for parsed_value in row {
|
|
81
|
+
let ruby_value = convert_parsed_to_ruby_value(parsed_value, &ruby)?;
|
|
82
|
+
row_array.push(ruby_value)?;
|
|
83
|
+
}
|
|
84
|
+
rows_array.push(row_array)?;
|
|
85
|
+
}
|
|
86
|
+
Ok(rows_array)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Ruby method: result.row_count
|
|
90
|
+
fn row_count(&self) -> usize {
|
|
91
|
+
self.row_count
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Ruby method: result.first - convenience method to get first row
|
|
95
|
+
fn first(&self) -> Result<Value, Error> {
|
|
96
|
+
let ruby = Ruby::get().map_err(|e| Error::new(Ruby::get().unwrap().exception_runtime_error(), format!("Ruby context unavailable: {}", e)))?;
|
|
97
|
+
|
|
98
|
+
if let Some(first_row) = self.raw_rows.first() {
|
|
99
|
+
let row_array = ruby.ary_new_capa(first_row.len());
|
|
100
|
+
for parsed_value in first_row {
|
|
101
|
+
let ruby_value = convert_parsed_to_ruby_value(parsed_value, &ruby)?;
|
|
102
|
+
row_array.push(ruby_value)?;
|
|
103
|
+
}
|
|
104
|
+
Ok(row_array.as_value())
|
|
105
|
+
} else {
|
|
106
|
+
Ok(ruby.qnil().as_value())
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Ruby method: result.each - iterate over rows
|
|
111
|
+
fn each(&self, block: Option<magnus::block::Proc>) -> Result<Value, Error> {
|
|
112
|
+
let ruby = Ruby::get().map_err(|e| Error::new(Ruby::get().unwrap().exception_runtime_error(), format!("Ruby context unavailable: {}", e)))?;
|
|
113
|
+
|
|
114
|
+
match block {
|
|
115
|
+
Some(proc) => {
|
|
116
|
+
for row in &self.raw_rows {
|
|
117
|
+
let row_array = ruby.ary_new_capa(row.len());
|
|
118
|
+
for parsed_value in row {
|
|
119
|
+
let ruby_value = convert_parsed_to_ruby_value(parsed_value, &ruby)?;
|
|
120
|
+
row_array.push(ruby_value)?;
|
|
121
|
+
}
|
|
122
|
+
proc.call::<(RArray,), Value>((row_array,))?;
|
|
123
|
+
}
|
|
124
|
+
Ok(ruby.qnil().as_value()) // Return nil for now
|
|
125
|
+
},
|
|
126
|
+
None => {
|
|
127
|
+
// Return enumerator - for now just return self
|
|
128
|
+
Ok(ruby.qnil().as_value()) // Return nil for now
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Convert ParsedValue to Ruby Value
|
|
135
|
+
fn convert_parsed_to_ruby_value(parsed_value: &ParsedValue, ruby: &Ruby) -> Result<Value, Error> {
|
|
136
|
+
match parsed_value {
|
|
137
|
+
ParsedValue::Null => Ok(ruby.qnil().as_value()),
|
|
138
|
+
ParsedValue::Bool(b) => Ok(ruby.into_value(*b)),
|
|
139
|
+
ParsedValue::Integer(i) => Ok(ruby.into_value(*i)),
|
|
140
|
+
ParsedValue::Float(f) => Ok(ruby.into_value(*f)),
|
|
141
|
+
ParsedValue::Text(s) => Ok(ruby.str_new(s).as_value()),
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// PGlite::Oxide.install!(path)
|
|
146
|
+
fn install(path: RString) -> Result<Value, Error> {
|
|
147
|
+
let ruby = Ruby::get().map_err(|e| Error::new(Ruby::get().unwrap().exception_runtime_error(), format!("Ruby context unavailable: {}", e)))?;
|
|
148
|
+
let path_str = path.to_string()?;
|
|
149
|
+
|
|
150
|
+
ensure_runtime();
|
|
151
|
+
|
|
152
|
+
match pglite_oxide::install_and_init_in(&path_str) {
|
|
153
|
+
Ok(_) => Ok(ruby.qnil().as_value()),
|
|
154
|
+
Err(e) => Err(Error::new(
|
|
155
|
+
ruby.exception_runtime_error(),
|
|
156
|
+
format!("Failed to install PGlite: {}", e)
|
|
157
|
+
))
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// PGlite::Oxide.version
|
|
162
|
+
fn version() -> Result<RString, Error> {
|
|
163
|
+
let ruby = Ruby::get().map_err(|e| Error::new(Ruby::get().unwrap().exception_runtime_error(), format!("Ruby context unavailable: {}", e)))?;
|
|
164
|
+
|
|
165
|
+
ensure_runtime();
|
|
166
|
+
|
|
167
|
+
match execute_query_internal("SELECT version()") {
|
|
168
|
+
Ok(result) => {
|
|
169
|
+
// Extract version from the first row, first column
|
|
170
|
+
if let Some(first_row) = result.rows.first() {
|
|
171
|
+
if let Some(first_value) = first_row.first() {
|
|
172
|
+
if let ParsedValue::Text(version_str) = first_value {
|
|
173
|
+
return Ok(ruby.str_new(version_str));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Fallback if parsing fails
|
|
179
|
+
Ok(ruby.str_new("PostgreSQL (via PGlite)"))
|
|
180
|
+
},
|
|
181
|
+
Err(e) => Err(Error::new(
|
|
182
|
+
ruby.exception_runtime_error(),
|
|
183
|
+
format!("Failed to get version: {}", e)
|
|
184
|
+
))
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// PGlite::Oxide.exec_query(sql) - returns QueryResult object
|
|
189
|
+
fn exec_query(sql: RString) -> Result<QueryResult, Error> {
|
|
190
|
+
let sql_str = sql.to_string()?;
|
|
191
|
+
|
|
192
|
+
ensure_runtime();
|
|
193
|
+
|
|
194
|
+
match execute_query_internal(&sql_str) {
|
|
195
|
+
Ok(parsed_result) => Ok(QueryResult::new(parsed_result)),
|
|
196
|
+
Err(e) => {
|
|
197
|
+
let ruby = Ruby::get().unwrap();
|
|
198
|
+
Err(Error::new(
|
|
199
|
+
ruby.exception_runtime_error(),
|
|
200
|
+
format!("Failed to execute query: {}", e)
|
|
201
|
+
))
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Internal function to execute queries
|
|
207
|
+
fn execute_query_internal(sql: &str) -> Result<ParsedResult, Box<dyn std::error::Error>> {
|
|
208
|
+
let input = pglite_oxide::interactive::PokeInput::Str(sql);
|
|
209
|
+
let response_bytes = pglite_oxide::interactive::exec_interactive(input)?;
|
|
210
|
+
|
|
211
|
+
// Parse the raw PostgreSQL protocol response
|
|
212
|
+
parse_postgres_response(response_bytes)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Parse raw PostgreSQL protocol response into ParsedResult
|
|
216
|
+
fn parse_postgres_response(raw_bytes: Vec<u8>) -> Result<ParsedResult, Box<dyn std::error::Error>> {
|
|
217
|
+
// Convert hex string to bytes if needed
|
|
218
|
+
let protocol_bytes = if let Ok(hex_str) = String::from_utf8(raw_bytes.clone()) {
|
|
219
|
+
// If it looks like hex (starts with hex chars), decode it
|
|
220
|
+
if hex_str.chars().all(|c| c.is_ascii_hexdigit() || c.is_whitespace()) {
|
|
221
|
+
hex::decode(hex_str.replace(|c: char| c.is_whitespace(), ""))
|
|
222
|
+
.unwrap_or(raw_bytes)
|
|
223
|
+
} else {
|
|
224
|
+
raw_bytes
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
raw_bytes
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// Parse the PostgreSQL wire protocol response
|
|
231
|
+
parse_query_response(&protocol_bytes)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Parse PostgreSQL wire protocol query response
|
|
235
|
+
fn parse_query_response(data: &[u8]) -> Result<ParsedResult, Box<dyn std::error::Error>> {
|
|
236
|
+
let mut offset = 0;
|
|
237
|
+
let mut columns = Vec::new();
|
|
238
|
+
let mut rows = Vec::new();
|
|
239
|
+
let mut command_tag = String::new();
|
|
240
|
+
|
|
241
|
+
while offset < data.len() {
|
|
242
|
+
if offset + 5 > data.len() {
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let msg_type = data[offset];
|
|
247
|
+
let length = u32::from_be_bytes([
|
|
248
|
+
data[offset + 1],
|
|
249
|
+
data[offset + 2],
|
|
250
|
+
data[offset + 3],
|
|
251
|
+
data[offset + 4]
|
|
252
|
+
]) as usize;
|
|
253
|
+
|
|
254
|
+
if offset + 1 + length > data.len() {
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let msg_data = &data[offset + 5..offset + 1 + length];
|
|
259
|
+
|
|
260
|
+
match msg_type {
|
|
261
|
+
b'T' => {
|
|
262
|
+
// RowDescription - column metadata
|
|
263
|
+
columns = parse_row_description(msg_data)?;
|
|
264
|
+
},
|
|
265
|
+
b'D' => {
|
|
266
|
+
// DataRow - actual data
|
|
267
|
+
if !columns.is_empty() {
|
|
268
|
+
let row = parse_data_row(msg_data, &columns)?;
|
|
269
|
+
rows.push(row);
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
b'C' => {
|
|
273
|
+
// CommandComplete
|
|
274
|
+
command_tag = parse_command_complete(msg_data)?;
|
|
275
|
+
},
|
|
276
|
+
b'Z' => {
|
|
277
|
+
// ReadyForQuery - end of response
|
|
278
|
+
break;
|
|
279
|
+
},
|
|
280
|
+
_ => {
|
|
281
|
+
// Skip unknown message types
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
offset += 1 + length;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let column_names = columns.into_iter().map(|col| col.name).collect();
|
|
289
|
+
Ok(ParsedResult {
|
|
290
|
+
command: command_tag,
|
|
291
|
+
columns: column_names,
|
|
292
|
+
rows,
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#[derive(Clone)]
|
|
297
|
+
struct ColumnInfo {
|
|
298
|
+
name: String,
|
|
299
|
+
oid: u32,
|
|
300
|
+
type_modifier: i32,
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
fn parse_row_description(data: &[u8]) -> Result<Vec<ColumnInfo>, Box<dyn std::error::Error>> {
|
|
304
|
+
let mut offset = 0;
|
|
305
|
+
|
|
306
|
+
if data.len() < 2 {
|
|
307
|
+
return Err("Invalid RowDescription: too short".into());
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let num_fields = u16::from_be_bytes([data[0], data[1]]);
|
|
311
|
+
offset += 2;
|
|
312
|
+
|
|
313
|
+
let mut columns = Vec::new();
|
|
314
|
+
|
|
315
|
+
for _ in 0..num_fields {
|
|
316
|
+
// Field name (null-terminated string)
|
|
317
|
+
let name_start = offset;
|
|
318
|
+
while offset < data.len() && data[offset] != 0 {
|
|
319
|
+
offset += 1;
|
|
320
|
+
}
|
|
321
|
+
if offset >= data.len() {
|
|
322
|
+
return Err("Invalid RowDescription: unterminated field name".into());
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let name = String::from_utf8_lossy(&data[name_start..offset]).to_string();
|
|
326
|
+
offset += 1; // skip null terminator
|
|
327
|
+
|
|
328
|
+
// Skip table OID (4 bytes), column number (2 bytes)
|
|
329
|
+
offset += 6;
|
|
330
|
+
|
|
331
|
+
if offset + 8 > data.len() {
|
|
332
|
+
return Err("Invalid RowDescription: insufficient data".into());
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Type OID (4 bytes)
|
|
336
|
+
let type_oid = u32::from_be_bytes([
|
|
337
|
+
data[offset], data[offset + 1], data[offset + 2], data[offset + 3]
|
|
338
|
+
]);
|
|
339
|
+
offset += 4;
|
|
340
|
+
|
|
341
|
+
// Skip type size (2 bytes)
|
|
342
|
+
offset += 2;
|
|
343
|
+
|
|
344
|
+
// Type modifier (4 bytes)
|
|
345
|
+
let type_modifier = i32::from_be_bytes([
|
|
346
|
+
data[offset], data[offset + 1], data[offset + 2], data[offset + 3]
|
|
347
|
+
]);
|
|
348
|
+
offset += 4;
|
|
349
|
+
|
|
350
|
+
// Skip format code (2 bytes)
|
|
351
|
+
offset += 2;
|
|
352
|
+
|
|
353
|
+
columns.push(ColumnInfo {
|
|
354
|
+
name,
|
|
355
|
+
oid: type_oid,
|
|
356
|
+
type_modifier,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
Ok(columns)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
fn parse_data_row(data: &[u8], columns: &[ColumnInfo]) -> Result<Vec<ParsedValue>, Box<dyn std::error::Error>> {
|
|
364
|
+
let mut offset = 0;
|
|
365
|
+
|
|
366
|
+
if data.len() < 2 {
|
|
367
|
+
return Err("Invalid DataRow: too short".into());
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
let num_fields = u16::from_be_bytes([data[0], data[1]]);
|
|
371
|
+
offset += 2;
|
|
372
|
+
|
|
373
|
+
let mut row_values = Vec::new();
|
|
374
|
+
|
|
375
|
+
for i in 0..num_fields as usize {
|
|
376
|
+
if offset + 4 > data.len() {
|
|
377
|
+
return Err("Invalid DataRow: insufficient length data".into());
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let field_len = i32::from_be_bytes([
|
|
381
|
+
data[offset], data[offset + 1], data[offset + 2], data[offset + 3]
|
|
382
|
+
]);
|
|
383
|
+
offset += 4;
|
|
384
|
+
|
|
385
|
+
let value = if field_len == -1 {
|
|
386
|
+
// NULL value
|
|
387
|
+
ParsedValue::Null
|
|
388
|
+
} else {
|
|
389
|
+
let field_len = field_len as usize;
|
|
390
|
+
if offset + field_len > data.len() {
|
|
391
|
+
return Err("Invalid DataRow: insufficient field data".into());
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
let field_data = &data[offset..offset + field_len];
|
|
395
|
+
offset += field_len;
|
|
396
|
+
|
|
397
|
+
// Convert based on PostgreSQL type
|
|
398
|
+
let column = columns.get(i).ok_or("Column index out of bounds")?;
|
|
399
|
+
convert_pg_value(field_data, column.oid)?
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
row_values.push(value);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
Ok(row_values)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
fn parse_command_complete(data: &[u8]) -> Result<String, Box<dyn std::error::Error>> {
|
|
409
|
+
// Command tag is a null-terminated string
|
|
410
|
+
let end = data.iter().position(|&b| b == 0).unwrap_or(data.len());
|
|
411
|
+
Ok(String::from_utf8_lossy(&data[..end]).to_string())
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
fn convert_pg_value(data: &[u8], type_oid: u32) -> Result<ParsedValue, Box<dyn std::error::Error>> {
|
|
415
|
+
match type_oid {
|
|
416
|
+
// Boolean
|
|
417
|
+
16 => Ok(ParsedValue::Bool(data[0] != 0)),
|
|
418
|
+
|
|
419
|
+
// Integers
|
|
420
|
+
20 => { // bigint (int8)
|
|
421
|
+
if data.len() != 8 {
|
|
422
|
+
return Err("Invalid bigint data length".into());
|
|
423
|
+
}
|
|
424
|
+
let val = i64::from_be_bytes(data.try_into()?);
|
|
425
|
+
Ok(ParsedValue::Integer(val))
|
|
426
|
+
},
|
|
427
|
+
21 => { // smallint (int2)
|
|
428
|
+
if data.len() != 2 {
|
|
429
|
+
return Err("Invalid smallint data length".into());
|
|
430
|
+
}
|
|
431
|
+
let val = i16::from_be_bytes(data.try_into()?);
|
|
432
|
+
Ok(ParsedValue::Integer(val as i64))
|
|
433
|
+
},
|
|
434
|
+
23 => { // integer (int4)
|
|
435
|
+
if data.len() != 4 {
|
|
436
|
+
return Err("Invalid integer data length".into());
|
|
437
|
+
}
|
|
438
|
+
let val = i32::from_be_bytes(data.try_into()?);
|
|
439
|
+
Ok(ParsedValue::Integer(val as i64))
|
|
440
|
+
},
|
|
441
|
+
|
|
442
|
+
// Floating point
|
|
443
|
+
700 => { // real (float4)
|
|
444
|
+
if data.len() != 4 {
|
|
445
|
+
return Err("Invalid float4 data length".into());
|
|
446
|
+
}
|
|
447
|
+
let val = f32::from_be_bytes(data.try_into()?);
|
|
448
|
+
Ok(ParsedValue::Float(val as f64))
|
|
449
|
+
},
|
|
450
|
+
701 => { // double precision (float8)
|
|
451
|
+
if data.len() != 8 {
|
|
452
|
+
return Err("Invalid float8 data length".into());
|
|
453
|
+
}
|
|
454
|
+
let val = f64::from_be_bytes(data.try_into()?);
|
|
455
|
+
Ok(ParsedValue::Float(val))
|
|
456
|
+
},
|
|
457
|
+
|
|
458
|
+
// Text types - most common fallback
|
|
459
|
+
25 | 1043 | 1042 | _ => {
|
|
460
|
+
// text, varchar, char, or unknown - treat as string
|
|
461
|
+
let str_val = String::from_utf8_lossy(data);
|
|
462
|
+
Ok(ParsedValue::Text(str_val.to_string()))
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
#[magnus::init]
|
|
468
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
469
|
+
let pglite_module = ruby.define_module("PGlite")?;
|
|
470
|
+
let oxide_class = pglite_module.define_class("Oxide", ruby.class_object())?;
|
|
471
|
+
|
|
472
|
+
// Define singleton methods (class methods)
|
|
473
|
+
oxide_class.define_singleton_method("install!", function!(install, 1))?;
|
|
474
|
+
oxide_class.define_singleton_method("version", function!(version, 0))?;
|
|
475
|
+
oxide_class.define_singleton_method("exec_query", function!(exec_query, 1))?;
|
|
476
|
+
|
|
477
|
+
// Define the Result class and its methods
|
|
478
|
+
let result_class = oxide_class.define_class("Result", ruby.class_object())?;
|
|
479
|
+
result_class.define_method("command", method!(QueryResult::command, 0))?;
|
|
480
|
+
result_class.define_method("columns", method!(QueryResult::columns, 0))?;
|
|
481
|
+
result_class.define_method("rows", method!(QueryResult::rows, 0))?;
|
|
482
|
+
result_class.define_method("row_count", method!(QueryResult::row_count, 0))?;
|
|
483
|
+
result_class.define_method("first", method!(QueryResult::first, 0))?;
|
|
484
|
+
result_class.define_method("each", method!(QueryResult::each, 1))?;
|
|
485
|
+
|
|
486
|
+
Ok(())
|
|
487
|
+
}
|
data/lib/pglite.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: pglite
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Vladimir Dementyev
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: bundler
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.15'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.15'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: combustion
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.1'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.1'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rake
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '13.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '13.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rake-compiler
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.2'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.2'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rb_sys
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0.9'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0.9'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: rspec
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '3.9'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '3.9'
|
|
96
|
+
description: Example description
|
|
97
|
+
email:
|
|
98
|
+
- Vladimir Dementyev
|
|
99
|
+
executables: []
|
|
100
|
+
extensions:
|
|
101
|
+
- ext/extconf.rb
|
|
102
|
+
extra_rdoc_files: []
|
|
103
|
+
files:
|
|
104
|
+
- CHANGELOG.md
|
|
105
|
+
- LICENSE.txt
|
|
106
|
+
- README.md
|
|
107
|
+
- ext/Cargo.lock
|
|
108
|
+
- ext/Cargo.toml
|
|
109
|
+
- ext/extconf.rb
|
|
110
|
+
- ext/src/lib.rs
|
|
111
|
+
- lib/pglite.rb
|
|
112
|
+
- lib/pglite/version.rb
|
|
113
|
+
homepage: https://github.com/palkan/pglite-rb
|
|
114
|
+
licenses:
|
|
115
|
+
- MIT
|
|
116
|
+
metadata:
|
|
117
|
+
bug_tracker_uri: https://github.com/palkan/pglite-rb/issues
|
|
118
|
+
changelog_uri: https://github.com/palkan/pglite-rb/blob/master/CHANGELOG.md
|
|
119
|
+
documentation_uri: https://github.com/palkan/pglite-rb
|
|
120
|
+
homepage_uri: https://github.com/palkan/pglite-rb
|
|
121
|
+
source_code_uri: https://github.com/palkan/pglite-rb
|
|
122
|
+
rdoc_options: []
|
|
123
|
+
require_paths:
|
|
124
|
+
- lib
|
|
125
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
126
|
+
requirements:
|
|
127
|
+
- - ">="
|
|
128
|
+
- !ruby/object:Gem::Version
|
|
129
|
+
version: '3.0'
|
|
130
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
131
|
+
requirements:
|
|
132
|
+
- - ">="
|
|
133
|
+
- !ruby/object:Gem::Version
|
|
134
|
+
version: '0'
|
|
135
|
+
requirements: []
|
|
136
|
+
rubygems_version: 3.6.9
|
|
137
|
+
specification_version: 4
|
|
138
|
+
summary: Example description
|
|
139
|
+
test_files: []
|