rubydb 0.1.1 → 0.1.2
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 +9 -0
- data/adapters/activerecord/lib/active_record/connection_adapters/rubydb_adapter.rb +9 -4
- data/adapters/activerecord/rubydb-activerecord.gemspec +1 -1
- data/adapters/activerecord/spec/rubydb_adapter_integration_spec.rb +17 -5
- data/lib/rubydb/execution/plan.rb +9 -8
- data/lib/rubydb/execution/planner.rb +14 -10
- data/lib/rubydb/sql/ast/insert.rb +27 -16
- data/lib/rubydb/sql/parser.rb +27 -17
- data/lib/rubydb/storage/engine.rb +24 -8
- data/lib/rubydb/version.rb +2 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5cfc85dc4d3cfca89bd7d89335e4bf887fbfca212836f8a62d92a70a63f0f284
|
|
4
|
+
data.tar.gz: 9869f03b9da1a7e0cfd0c01f1f22ae9b5ba0e2b12eedaeac72e1b4deab878a0b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 76650c2403e26244619687eb6146bb7a1561d3764d664094a6deb94e02566924c60979f481aca972be026987775f8c9b68ed02804d15e583f885528e02833613
|
|
7
|
+
data.tar.gz: 60c894fde163c1a47ed84aa2c2d8b374a9b6c70f3d810feb7ca419a06aa2cb5e6026d8aa278d27ed89ad4a83b48405c31137671f1d2dab7240d0efc36b367de8
|
data/CHANGELOG.md
CHANGED
|
@@ -23,6 +23,15 @@ All notable changes to RubyDB are documented here. Versions follow
|
|
|
23
23
|
vacuum, maintenance, and release workflows.
|
|
24
24
|
- Clarified the tested common SQLite-style profile and production limits.
|
|
25
25
|
|
|
26
|
+
## 0.1.2 - 2026-09-09
|
|
27
|
+
|
|
28
|
+
- Added SQLite/ActiveRecord-compatible `INSERT ... DEFAULT VALUES` parsing,
|
|
29
|
+
planning, execution, default materialization, and regression coverage.
|
|
30
|
+
- Normalized literal schema defaults before they reach physical rows so empty
|
|
31
|
+
strings and other scalar defaults are not persisted as AST wrapper objects.
|
|
32
|
+
- Released `rubydb-activerecord` 0.1.1 with scalar default unwrapping for
|
|
33
|
+
ActiveRecord column metadata.
|
|
34
|
+
|
|
26
35
|
## 0.1.1 - 2026-09-09
|
|
27
36
|
|
|
28
37
|
- Published the Rails connection configuration fixes for embedded database
|
|
@@ -756,10 +756,15 @@ module ActiveRecord
|
|
|
756
756
|
|
|
757
757
|
# ActiveRecord's generic Column deduplication is string-oriented. RubyDB
|
|
758
758
|
# persists typed defaults, so serialize scalar defaults at this boundary
|
|
759
|
-
# and let ActiveRecord cast them through the column type map.
|
|
760
|
-
def rails_default_value(value)
|
|
761
|
-
|
|
762
|
-
|
|
759
|
+
# and let ActiveRecord cast them through the column type map.
|
|
760
|
+
def rails_default_value(value)
|
|
761
|
+
# RubyDB exposes SQL defaults as AST literals. ActiveRecord expects
|
|
762
|
+
# the scalar payload when it builds its Column metadata; calling
|
|
763
|
+
# `to_s` on the wrapper would leak the Ruby object inspection into
|
|
764
|
+
# newly instantiated records.
|
|
765
|
+
value = value.value if value.respond_to?(:value) && !value.is_a?(String)
|
|
766
|
+
value.is_a?(String) ? value : value.to_s
|
|
767
|
+
end
|
|
763
768
|
|
|
764
769
|
def sql_for_execution(sql)
|
|
765
770
|
sql = sql.to_sql if sql.respond_to?(:to_sql)
|
|
@@ -51,7 +51,7 @@ RSpec.describe ActiveRecord::ConnectionAdapters::RubyDBAdapter do
|
|
|
51
51
|
expect(result.first).to include("enabled" => false, "attempts" => 0)
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
it "runs a Rails migration that creates a table, adds a column, and adds an index" do
|
|
54
|
+
it "runs a Rails migration that creates a table, adds a column, and adds an index" do
|
|
55
55
|
migration = Class.new(ActiveRecord::Migration[migration_version]) do
|
|
56
56
|
def change
|
|
57
57
|
create_table :projects do |table|
|
|
@@ -74,10 +74,22 @@ RSpec.describe ActiveRecord::ConnectionAdapters::RubyDBAdapter do
|
|
|
74
74
|
expect(schema).not_to include('t.integer "id"')
|
|
75
75
|
|
|
76
76
|
migration.new.migrate(:down)
|
|
77
|
-
expect(connection.table_exists?(:projects)).to be(false)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
it "
|
|
77
|
+
expect(connection.table_exists?(:projects)).to be(false)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "exposes scalar schema defaults to ActiveRecord" do
|
|
81
|
+
connection = ActiveRecord::Base.connection
|
|
82
|
+
connection.execute("CREATE TABLE settings (id INTEGER PRIMARY KEY, label VARCHAR(255) NOT NULL DEFAULT '')")
|
|
83
|
+
|
|
84
|
+
settings_model = Class.new(ActiveRecord::Base) do
|
|
85
|
+
self.table_name = "settings"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
expect(settings_model.new.label).to eq("")
|
|
89
|
+
expect(settings_model.create!.reload.label).to eq("")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "executes an ActiveRecord association join with qualified filtering" do
|
|
81
93
|
stub_const("RubydbAccount", Class.new(ActiveRecord::Base) do
|
|
82
94
|
self.table_name = "accounts"
|
|
83
95
|
has_many :rubydb_projects, class_name: "RubydbProject", foreign_key: :account_id
|
|
@@ -124,14 +124,15 @@ module RubyDB
|
|
|
124
124
|
end
|
|
125
125
|
|
|
126
126
|
class Insert < Plan
|
|
127
|
-
attr_reader :values, :rows, :on_conflict
|
|
128
|
-
|
|
129
|
-
def initialize(table_name, columns = [], values = [], rows: nil, on_conflict: nil)
|
|
130
|
-
super(:insert, table_name, columns)
|
|
131
|
-
@rows = rows || [values]
|
|
132
|
-
@values = @rows.first || []
|
|
133
|
-
@on_conflict = on_conflict
|
|
134
|
-
|
|
127
|
+
attr_reader :values, :rows, :on_conflict, :default_values
|
|
128
|
+
|
|
129
|
+
def initialize(table_name, columns = [], values = [], rows: nil, on_conflict: nil, default_values: false)
|
|
130
|
+
super(:insert, table_name, columns)
|
|
131
|
+
@rows = rows || [values]
|
|
132
|
+
@values = @rows.first || []
|
|
133
|
+
@on_conflict = on_conflict
|
|
134
|
+
@default_values = default_values
|
|
135
|
+
end
|
|
135
136
|
end
|
|
136
137
|
|
|
137
138
|
class Update < Plan
|
|
@@ -154,12 +154,13 @@ module RubyDB
|
|
|
154
154
|
|
|
155
155
|
def plan_insert(statement)
|
|
156
156
|
Plan::Insert.new(
|
|
157
|
-
statement.table,
|
|
158
|
-
statement.columns,
|
|
159
|
-
statement.values,
|
|
160
|
-
rows: statement.rows,
|
|
161
|
-
on_conflict: statement.on_conflict
|
|
162
|
-
|
|
157
|
+
statement.table,
|
|
158
|
+
statement.columns,
|
|
159
|
+
statement.values,
|
|
160
|
+
rows: statement.rows,
|
|
161
|
+
on_conflict: statement.on_conflict,
|
|
162
|
+
default_values: statement.default_values?
|
|
163
|
+
)
|
|
163
164
|
end
|
|
164
165
|
|
|
165
166
|
def plan_update(statement)
|
|
@@ -186,10 +187,13 @@ module RubyDB
|
|
|
186
187
|
plan
|
|
187
188
|
end
|
|
188
189
|
|
|
189
|
-
def plan_create_table(statement)
|
|
190
|
-
columns = statement.columns.map do |col|
|
|
191
|
-
|
|
192
|
-
|
|
190
|
+
def plan_create_table(statement)
|
|
191
|
+
columns = statement.columns.map do |col|
|
|
192
|
+
options = col.options.dup
|
|
193
|
+
default = options[:default]
|
|
194
|
+
options[:default] = default.value if default.respond_to?(:value) && !default.is_a?(String)
|
|
195
|
+
Catalog::Column.new(col.name, col.type_class, **options)
|
|
196
|
+
end
|
|
193
197
|
|
|
194
198
|
Plan::CreateTable.new(
|
|
195
199
|
statement.name,
|
|
@@ -5,15 +5,17 @@ module RubyDB
|
|
|
5
5
|
module AST
|
|
6
6
|
# INSERT statement AST node
|
|
7
7
|
class Insert < Node
|
|
8
|
-
attr_reader :table, :columns, :values, :rows, :on_conflict
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@
|
|
14
|
-
@
|
|
15
|
-
@
|
|
16
|
-
@
|
|
8
|
+
attr_reader :table, :columns, :values, :rows, :on_conflict
|
|
9
|
+
attr_reader :default_values
|
|
10
|
+
|
|
11
|
+
def initialize(table, columns = [], values = [], rows: nil, on_conflict: nil, default_values: false, location: nil)
|
|
12
|
+
super(location: location)
|
|
13
|
+
@table = table
|
|
14
|
+
@columns = columns
|
|
15
|
+
@rows = rows || [values]
|
|
16
|
+
@values = @rows.first || []
|
|
17
|
+
@on_conflict = on_conflict
|
|
18
|
+
@default_values = default_values
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
def accept(visitor)
|
|
@@ -24,8 +26,9 @@ module RubyDB
|
|
|
24
26
|
Insert.new(
|
|
25
27
|
@table,
|
|
26
28
|
@columns.dup,
|
|
27
|
-
@values.map(&:clone), rows: @rows.map { |row| row.map(&:clone) }, on_conflict: @on_conflict,
|
|
28
|
-
|
|
29
|
+
@values.map(&:clone), rows: @rows.map { |row| row.map(&:clone) }, on_conflict: @on_conflict,
|
|
30
|
+
default_values: @default_values,
|
|
31
|
+
location: @location
|
|
29
32
|
)
|
|
30
33
|
end
|
|
31
34
|
|
|
@@ -37,8 +40,12 @@ module RubyDB
|
|
|
37
40
|
parts << "(#{@columns.join(", ")})"
|
|
38
41
|
end
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
if @default_values
|
|
44
|
+
parts << "DEFAULT VALUES"
|
|
45
|
+
else
|
|
46
|
+
parts << "VALUES"
|
|
47
|
+
parts << @rows.map { |row| "(#{row.map(&:to_sql).join(", ")})" }.join(", ")
|
|
48
|
+
end
|
|
42
49
|
parts << "ON CONFLICT DO NOTHING" if @on_conflict == :nothing
|
|
43
50
|
if @on_conflict.is_a?(Hash) && @on_conflict[:action] == :update
|
|
44
51
|
target = @on_conflict[:target].any? ? " (#{@on_conflict[:target].join(', ')})" : ""
|
|
@@ -56,9 +63,13 @@ module RubyDB
|
|
|
56
63
|
end
|
|
57
64
|
|
|
58
65
|
# Helper methods
|
|
59
|
-
def has_columns?
|
|
60
|
-
@columns.any?
|
|
61
|
-
end
|
|
66
|
+
def has_columns?
|
|
67
|
+
@columns.any?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def default_values?
|
|
71
|
+
@default_values
|
|
72
|
+
end
|
|
62
73
|
|
|
63
74
|
def value_count
|
|
64
75
|
@values.size
|
data/lib/rubydb/sql/parser.rb
CHANGED
|
@@ -539,21 +539,30 @@ module RubyDB
|
|
|
539
539
|
expect(Token::Type::RPAREN)
|
|
540
540
|
end
|
|
541
541
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
expect(Token::Type::
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
542
|
+
default_values = false
|
|
543
|
+
if current_token&.type == Token::Type::DEFAULT
|
|
544
|
+
advance
|
|
545
|
+
expect(Token::Type::VALUES)
|
|
546
|
+
default_values = true
|
|
547
|
+
else
|
|
548
|
+
expect(Token::Type::VALUES)
|
|
549
|
+
end
|
|
550
|
+
rows = default_values ? [[]] : []
|
|
551
|
+
unless default_values
|
|
552
|
+
loop do
|
|
553
|
+
expect(Token::Type::LPAREN)
|
|
554
|
+
values = []
|
|
555
|
+
while true
|
|
556
|
+
values << parse_expression
|
|
557
|
+
break unless current_token&.type == Token::Type::COMMA
|
|
558
|
+
advance
|
|
559
|
+
end
|
|
560
|
+
expect(Token::Type::RPAREN)
|
|
561
|
+
rows << values
|
|
562
|
+
break unless current_token&.type == Token::Type::COMMA
|
|
563
|
+
advance
|
|
564
|
+
end
|
|
565
|
+
end
|
|
557
566
|
|
|
558
567
|
on_conflict = nil
|
|
559
568
|
if current_token&.type == Token::Type::ON
|
|
@@ -589,8 +598,9 @@ module RubyDB
|
|
|
589
598
|
raise ParserError, "Expected NOTHING or UPDATE after ON CONFLICT DO"
|
|
590
599
|
end
|
|
591
600
|
end
|
|
592
|
-
AST::Insert.new(table, columns, rows.first || [], rows: rows, on_conflict: on_conflict
|
|
593
|
-
|
|
601
|
+
AST::Insert.new(table, columns, rows.first || [], rows: rows, on_conflict: on_conflict,
|
|
602
|
+
default_values: default_values)
|
|
603
|
+
end
|
|
594
604
|
|
|
595
605
|
def parse_update
|
|
596
606
|
expect(Token::Type::UPDATE)
|
|
@@ -675,16 +675,32 @@ module RubyDB
|
|
|
675
675
|
end
|
|
676
676
|
|
|
677
677
|
# Row operations
|
|
678
|
-
def insert_row(table_name, columns, values)
|
|
679
|
-
ensure_writable!
|
|
680
|
-
table_name = resolve_table_name(table_name)
|
|
681
|
-
@lock.synchronize do
|
|
678
|
+
def insert_row(table_name, columns, values)
|
|
679
|
+
ensure_writable!
|
|
680
|
+
table_name = resolve_table_name(table_name)
|
|
681
|
+
@lock.synchronize do
|
|
682
682
|
@stats[:row_inserts] += 1
|
|
683
683
|
|
|
684
|
-
metadata = @table_metadata[table_name]
|
|
685
|
-
raise DatabaseError, "Table '#{table_name}' does not exist" unless metadata
|
|
686
|
-
|
|
687
|
-
#
|
|
684
|
+
metadata = @table_metadata[table_name]
|
|
685
|
+
raise DatabaseError, "Table '#{table_name}' does not exist" unless metadata
|
|
686
|
+
|
|
687
|
+
# SQL INSERT may omit columns entirely (DEFAULT VALUES) or omit
|
|
688
|
+
# only selected columns. Materialize declared defaults before
|
|
689
|
+
# constraint validation so NOT NULL defaults are valid and the
|
|
690
|
+
# physical row contains scalar values rather than AST wrappers.
|
|
691
|
+
if values.is_a?(Hash)
|
|
692
|
+
values = values.dup
|
|
693
|
+
columns.each do |column|
|
|
694
|
+
present = values.key?(column.name) || values.key?(column.name.to_sym)
|
|
695
|
+
next if present || !column.has_default?
|
|
696
|
+
|
|
697
|
+
default = column.default
|
|
698
|
+
default = default.value if default.respond_to?(:value)
|
|
699
|
+
values[column.name] = default
|
|
700
|
+
end
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
# Rails and other SQL clients omit an INTEGER primary key on insert
|
|
688
704
|
# and expect the database to allocate it. Keep allocation in the
|
|
689
705
|
# engine so embedded and server connections have identical behavior.
|
|
690
706
|
if values.is_a?(Hash)
|
data/lib/rubydb/version.rb
CHANGED
|
@@ -11,13 +11,13 @@ module RubyDB
|
|
|
11
11
|
# - MAJOR: Incompatible API changes
|
|
12
12
|
# - MINOR: Backwards-compatible new functionality
|
|
13
13
|
# - PATCH: Backwards-compatible bug fixes
|
|
14
|
-
VERSION = "0.1.
|
|
14
|
+
VERSION = "0.1.2"
|
|
15
15
|
|
|
16
16
|
# Version components for easy access
|
|
17
17
|
module Version
|
|
18
18
|
MAJOR = 0
|
|
19
19
|
MINOR = 1
|
|
20
|
-
PATCH =
|
|
20
|
+
PATCH = 2
|
|
21
21
|
PRE = nil # e.g., "alpha", "beta", "rc1"
|
|
22
22
|
|
|
23
23
|
def self.to_s
|