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.
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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rb_sys/mkmf"
5
+
6
+ create_rust_makefile("pglite-rb/pglite_rb_ext")
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
+ }
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PGlite # :nodoc:
4
+ VERSION = "0.0.1"
5
+ end
data/lib/pglite.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PGlite
4
+ # Main PGlite module
5
+ end
6
+
7
+ require "pglite/version"
8
+
9
+ begin
10
+ require "pglite_rb_ext"
11
+ rescue LoadError
12
+ # Extension not available
13
+ end
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: []