Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/go-images/images v0.0.0-20260620184442-aa6cd1c0beb7
github.com/go-ndarray/ndarray v0.0.0-20260620170009-555bfc31e7a3
github.com/go-ruby-abbrev/abbrev v0.0.0-20260629150957-97117892cd38
github.com/go-ruby-activerecord/activerecord v0.0.0-20260702164905-aee9eeac33f8
github.com/go-ruby-addressable/addressable v0.0.0-20260701121828-b1a644c57795
github.com/go-ruby-base64/base64 v0.0.0-20260630081041-5dec54f20f53
github.com/go-ruby-bcrypt/bcrypt v0.0.0-20260701122042-7e14b6a42363
Expand All @@ -33,6 +34,7 @@ require (
github.com/go-ruby-find/find v0.0.0-20260630081030-35072d185272
github.com/go-ruby-format/format v0.0.0-20260629094031-e294417ab31c
github.com/go-ruby-getoptlong/getoptlong v0.0.0-20260629150025-1a1bfd19bc49
github.com/go-ruby-grape/grape v0.0.0-20260702151528-455377c8c7c3
github.com/go-ruby-haml/haml v0.0.0-20260701125233-5bf8084caf1c
github.com/go-ruby-hcl2/hcl2 v0.0.0-20260630160546-4b7ef3837e5b
github.com/go-ruby-ipaddr/ipaddr v0.0.0-20260630052208-78ca85dc054b
Expand All @@ -49,7 +51,7 @@ require (
github.com/go-ruby-money/money v0.0.0-20260702143724-59c9de931e83
github.com/go-ruby-msgpack/msgpack v0.0.0-20260630150113-002078d2af90
github.com/go-ruby-mustache/mustache v0.0.0-20260701123847-26d5e451677a
github.com/go-ruby-nokogiri/nokogiri v0.0.0-20260702151113-e53746446651
github.com/go-ruby-nokogiri/nokogiri v0.0.0-20260702164556-6e939959240e
github.com/go-ruby-oauth2/oauth2 v0.0.0-20260702151234-88fab8d845a1
github.com/go-ruby-observer/observer v0.0.0-20260630080708-c3a02da51f79
github.com/go-ruby-optparse/optparse v0.0.0-20260629093110-6b69a6b03546
Expand All @@ -68,6 +70,7 @@ require (
github.com/go-ruby-rouge/rouge v0.0.0-20260701044002-71f9c1aaa66c
github.com/go-ruby-rqrcode/rqrcode v0.0.0-20260701142854-896858beadc8
github.com/go-ruby-rspec/rspec v0.0.0-20260702145830-12badaeb0d75
github.com/go-ruby-rubocop/rubocop v0.0.0-20260702170528-0a89da6e9147
github.com/go-ruby-scanf/scanf v0.0.0-20260629150220-414dbb31c386
github.com/go-ruby-securerandom/securerandom v0.0.0-20260630081933-3f81ff7d7fb0
github.com/go-ruby-sequel/sequel v0.0.0-20260702151352-66413b601977
Expand All @@ -81,6 +84,7 @@ require (
github.com/go-ruby-tzinfo/tzinfo v0.0.0-20260701105256-15977bdf6e1a
github.com/go-ruby-unicode-normalize/unicode-normalize v0.0.0-20260629152419-984d3fbcfb7f
github.com/go-ruby-uri/uri v0.0.0-20260629113958-59633d1b0deb
github.com/go-ruby-xslt/xslt v0.0.0-20260702171958-146eaf3f0176
github.com/go-ruby-yaml/yaml v0.0.0-20260629093916-8035038027bd
github.com/go-ruby-zlib/zlib v0.0.0-20260630052127-b4ac0c1ab281
)
Expand Down
12 changes: 10 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ github.com/go-ndarray/ndarray v0.0.0-20260620170009-555bfc31e7a3 h1:oqUg3BkpFqPq
github.com/go-ndarray/ndarray v0.0.0-20260620170009-555bfc31e7a3/go.mod h1:CZPf0P3FjwYlphONFD0ZAjE+/eaixCZYvYhm6QqmqsU=
github.com/go-ruby-abbrev/abbrev v0.0.0-20260629150957-97117892cd38 h1:j0VAUKCGNUTZVThG4Pi3pgdHXFybwXnY7anpYuKeuH4=
github.com/go-ruby-abbrev/abbrev v0.0.0-20260629150957-97117892cd38/go.mod h1:PgAtItdQBv87Wog0Q7rnbwhqsupDC0pDXDlZgSv6nvA=
github.com/go-ruby-activerecord/activerecord v0.0.0-20260702164905-aee9eeac33f8 h1:Wch5abT03FDXqmfbicJ+obeRP0brwY3DzvE6irEisZI=
github.com/go-ruby-activerecord/activerecord v0.0.0-20260702164905-aee9eeac33f8/go.mod h1:0QY+Ri6/HhOkjRGhhyZ5NwtcDosKI7xwO5csmWEG0W0=
github.com/go-ruby-addressable/addressable v0.0.0-20260701121828-b1a644c57795 h1:UPDrh2pXuwewmbZiEM5SWg26Hy/08mw4MJnCszwGUEk=
github.com/go-ruby-addressable/addressable v0.0.0-20260701121828-b1a644c57795/go.mod h1:4gG3enh3yJ4fpQWjNuzsLbB01vF8XINKkgqsHdfL9O4=
github.com/go-ruby-base64/base64 v0.0.0-20260630081041-5dec54f20f53 h1:MBggShyLWDpr0USQMZvcRvRc+OIcHKFn4ohDnmB3C6k=
Expand Down Expand Up @@ -74,6 +76,8 @@ github.com/go-ruby-format/format v0.0.0-20260629094031-e294417ab31c h1:dJBMxMFxB
github.com/go-ruby-format/format v0.0.0-20260629094031-e294417ab31c/go.mod h1:1tuz12SHPOcnFSfdPYRFwVvRhMT4Xn4M4jfPFgM7pLA=
github.com/go-ruby-getoptlong/getoptlong v0.0.0-20260629150025-1a1bfd19bc49 h1:v2Cy3bZTTi+u9XKBRP8FmYHwsWwZ+t8hlh3s4i1gGfQ=
github.com/go-ruby-getoptlong/getoptlong v0.0.0-20260629150025-1a1bfd19bc49/go.mod h1:nGvVSBSIsYrhM2qs4yQ2k8uDbXCFPWQXXqkowleHMDs=
github.com/go-ruby-grape/grape v0.0.0-20260702151528-455377c8c7c3 h1:JPcmRUa/fiwj55H7LXULvrxifCaQkH6fXTvDZZg3pvw=
github.com/go-ruby-grape/grape v0.0.0-20260702151528-455377c8c7c3/go.mod h1:N0dFV4BDu87D5qFusrC2jUb+hPlJpiAuuHRSMznMM7c=
github.com/go-ruby-haml/haml v0.0.0-20260701125233-5bf8084caf1c h1:VByP5jdzov6JMMsIPremuz4Q06y/rSCac/j5Q3GvXZk=
github.com/go-ruby-haml/haml v0.0.0-20260701125233-5bf8084caf1c/go.mod h1:D4xOrc0xIiN0u0Q+KqjfKx/f7up8ekpud/syYsHTrPY=
github.com/go-ruby-hcl2/hcl2 v0.0.0-20260630160546-4b7ef3837e5b h1:hi8tu1lr/1cOkFMdaONWyuNGVmE9GuN8tmpUN6VxgLI=
Expand Down Expand Up @@ -106,8 +110,8 @@ github.com/go-ruby-msgpack/msgpack v0.0.0-20260630150113-002078d2af90 h1:xjyxDgC
github.com/go-ruby-msgpack/msgpack v0.0.0-20260630150113-002078d2af90/go.mod h1:MkpM6qH0Zz8H6btdGOPBmnImw6BbpcMCvPHV9J2V2Rc=
github.com/go-ruby-mustache/mustache v0.0.0-20260701123847-26d5e451677a h1:mZr5L/r+Qw0RHzFnwV/2jB2GPm/dOcaoki6/Rz34eCw=
github.com/go-ruby-mustache/mustache v0.0.0-20260701123847-26d5e451677a/go.mod h1:Jk5CZ+5dR63295HwuIRn+MOz/RwGvZvy5UEJpoAiGkk=
github.com/go-ruby-nokogiri/nokogiri v0.0.0-20260702151113-e53746446651 h1:5zEQVymgZTTJMmfAf66r8RtIyhjZhX/LEvISYfYLo0A=
github.com/go-ruby-nokogiri/nokogiri v0.0.0-20260702151113-e53746446651/go.mod h1:WsLJ6/u8/nzHShZKChf20tHosn/nlVnOJiD/xEY6X8c=
github.com/go-ruby-nokogiri/nokogiri v0.0.0-20260702164556-6e939959240e h1:v8DyCc3Vp+REhN1uyQ4wN8CpJ6JMqWNOm9OESWFBtLU=
github.com/go-ruby-nokogiri/nokogiri v0.0.0-20260702164556-6e939959240e/go.mod h1:WsLJ6/u8/nzHShZKChf20tHosn/nlVnOJiD/xEY6X8c=
github.com/go-ruby-oauth2/oauth2 v0.0.0-20260702151234-88fab8d845a1 h1:iZFtIn5WHQRz/oa04qYAlLvTi0XDRP5dKPGpJTGRzKg=
github.com/go-ruby-oauth2/oauth2 v0.0.0-20260702151234-88fab8d845a1/go.mod h1:/yz3iBLTlChPKMqrP0uA5tBpXKh4a8Rwd46SAv/lMiw=
github.com/go-ruby-observer/observer v0.0.0-20260630080708-c3a02da51f79 h1:xZIEcf1IAtrI81CRyflqoeqkbTz7CUd3aAicW9HDzfY=
Expand Down Expand Up @@ -144,6 +148,8 @@ github.com/go-ruby-rqrcode/rqrcode v0.0.0-20260701142854-896858beadc8 h1:02tvTxe
github.com/go-ruby-rqrcode/rqrcode v0.0.0-20260701142854-896858beadc8/go.mod h1:+MJBJltcK0b7tztRFluEcmlg4PVSA23h4LTbsBu7Aks=
github.com/go-ruby-rspec/rspec v0.0.0-20260702145830-12badaeb0d75 h1:EC/j4IhxcBe+9A8hdW1A4QrRCMaTSdvKhP2n0ahWqYA=
github.com/go-ruby-rspec/rspec v0.0.0-20260702145830-12badaeb0d75/go.mod h1:BYeb8EVnYLyl+ykaRyA1XAKJl3bOad/hJ7D+ZZCj2SQ=
github.com/go-ruby-rubocop/rubocop v0.0.0-20260702170528-0a89da6e9147 h1:853nYYolC7/2Q/3Cd2mj9Ck+gRSpU5cWgCIJn4QfWCU=
github.com/go-ruby-rubocop/rubocop v0.0.0-20260702170528-0a89da6e9147/go.mod h1:B3YLDBNxYrrd94jXv++1Fsxw/SsjtoZT6hpF6OZ+wFY=
github.com/go-ruby-scanf/scanf v0.0.0-20260629150220-414dbb31c386 h1:mt488Km0GMk/yjJtKUY4jo+fNz1xqHe0uLbEwPJa4To=
github.com/go-ruby-scanf/scanf v0.0.0-20260629150220-414dbb31c386/go.mod h1:UpBHz/Hf4Q8h14UpfJ5Cpp+OxVEqqNuICXGnUjg2dKc=
github.com/go-ruby-securerandom/securerandom v0.0.0-20260630081933-3f81ff7d7fb0 h1:NDJ7mJ3uMoysAoBRMJJNr0RRbqLfzp+8flALEeOuahI=
Expand All @@ -170,6 +176,8 @@ github.com/go-ruby-unicode-normalize/unicode-normalize v0.0.0-20260629152419-984
github.com/go-ruby-unicode-normalize/unicode-normalize v0.0.0-20260629152419-984d3fbcfb7f/go.mod h1:mKlLb/MTNhSHSUhcAvb6hi3ZCR5kCgVdDaInEyI3Wr8=
github.com/go-ruby-uri/uri v0.0.0-20260629113958-59633d1b0deb h1:CyoFEEsMPa/TX4EJHOPsXud3nccESDhaW6fTQsidIKI=
github.com/go-ruby-uri/uri v0.0.0-20260629113958-59633d1b0deb/go.mod h1:/7+NF+4kESzcNHjRtNe/lAfhgHpgElX4C+VNo0xsb6Y=
github.com/go-ruby-xslt/xslt v0.0.0-20260702171958-146eaf3f0176 h1:iHTCF/Eu0/lwOEank8pLKqoW5j9MI2ERXErrvqsZZ+4=
github.com/go-ruby-xslt/xslt v0.0.0-20260702171958-146eaf3f0176/go.mod h1:1C3Bx0pQIsqLqC2SnqV7pJnGuLJsgYmar2NrD+CmVXw=
github.com/go-ruby-yaml/yaml v0.0.0-20260629093916-8035038027bd h1:4HGjgCdTIuIEM+iZXJ9hJJzt0IBMtxPqdJq72mr0Y64=
github.com/go-ruby-yaml/yaml v0.0.0-20260629093916-8035038027bd/go.mod h1:HtbQgnZpSTJBAhPg1jOKgHycZl3tXgf9ZdOu4dnHWjQ=
github.com/go-ruby-zlib/zlib v0.0.0-20260630052127-b4ac0c1ab281 h1:zOn3ro0WBggDR0xqvNSdOPxHEWzdR6iMy842CAyIL/g=
Expand Down
137 changes: 137 additions & 0 deletions internal/vm/activerecord.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Copyright (c) the go-embedded-ruby/ruby authors
//
// SPDX-License-Identifier: BSD-3-Clause

package vm

import (
activerecord "github.com/go-ruby-activerecord/activerecord"

"github.com/go-embedded-ruby/ruby/internal/object"
)

// ActiveRecordModel is the Ruby wrapper around a *activerecord.Model — a mapped
// model class (its table, columns, validations and associations). The
// query-building + schema-DDL + validations core lives in the
// github.com/go-ruby-activerecord/activerecord library, which renders SQL
// byte-faithful to ActiveRecord::Relation#to_sql. Actual database execution is a
// host seam wired here to go-ruby-sqlite3 (see activerecord_adapter.go), so a
// relation's #to_a / #count / #exists? / #pluck run against a real database once
// ActiveRecord::Base.establish_connection has opened one.
type ActiveRecordModel struct {
m *activerecord.Model
cls *RClass
}

func (m *ActiveRecordModel) ToS() string { return "#<ActiveRecord::Model " + m.m.Name + ">" }
func (m *ActiveRecordModel) Inspect() string { return m.ToS() }
func (m *ActiveRecordModel) Truthy() bool { return true }

// ActiveRecordRelation is the Ruby wrapper around a *activerecord.Relation — a
// lazy, chainable query (every refining method returns a new relation). #to_sql
// renders it; the execution methods run it through the connected adapter.
type ActiveRecordRelation struct {
r *activerecord.Relation
model *ActiveRecordModel
}

func (r *ActiveRecordRelation) ToS() string { return r.r.ToSQL() }
func (r *ActiveRecordRelation) Inspect() string { return "#<ActiveRecord::Relation>" }
func (r *ActiveRecordRelation) Truthy() bool { return true }

// ActiveRecordRecord is the Ruby wrapper around a *activerecord.Record — a single
// model instance's attribute set with dirty tracking and validations.
type ActiveRecordRecord struct {
rec *activerecord.Record
model *ActiveRecordModel
}

func (r *ActiveRecordRecord) ToS() string { return "#<ActiveRecord::Record>" }
func (r *ActiveRecordRecord) Inspect() string { return "#<ActiveRecord::Record>" }
func (r *ActiveRecordRecord) Truthy() bool { return true }

// ActiveRecordErrors is the Ruby wrapper around a *activerecord.Errors — the
// ActiveModel::Errors shape a validation produces (#full_messages / #messages /
// #[] / #empty? / #count).
type ActiveRecordErrors struct {
e *activerecord.Errors
}

func (e *ActiveRecordErrors) ToS() string { return "#<ActiveRecord::Errors>" }
func (e *ActiveRecordErrors) Inspect() string { return e.ToS() }
func (e *ActiveRecordErrors) Truthy() bool { return true }

// ActiveRecordModelBuilder is the DSL self a `ActiveRecord::Model.new(name, table)
// { … }` block runs against: #column declares columns and the validates_* /
// belongs_to / has_many methods declare validations and associations.
type ActiveRecordModelBuilder struct {
m *activerecord.Model
}

func (b *ActiveRecordModelBuilder) ToS() string { return "#<ActiveRecord::Model::DSL>" }
func (b *ActiveRecordModelBuilder) Inspect() string { return b.ToS() }
func (b *ActiveRecordModelBuilder) Truthy() bool { return true }

// registerActiveRecord installs the ActiveRecord module and its Model / Relation
// / Record / Errors surface (require "active_record"): ActiveRecord::Base
// .establish_connection(database:) opens a sqlite3 connection (the adapter seam);
// ActiveRecord::Model.new(name, table) { column …; validates … } builds a model
// whose #where/#select/#order/#limit/#offset/#group/#having/#joins/#distinct/#not
// return chainable relations, #to_sql renders them, and #to_a/#count/#exists?/
// #pluck run through the adapter; ActiveRecord::Record carries a validating
// attribute set. The StatementInvalid / RecordInvalid error tree matches the gem.
func (vm *VM) registerActiveRecord() {
mod := newClass("ActiveRecord", nil)
mod.isModule = true
vm.consts["ActiveRecord"] = mod

vm.registerActiveRecordErrors(mod)
vm.registerActiveRecordBase(mod)
vm.registerActiveRecordModel(mod)
vm.registerActiveRecordRelation(mod)
vm.registerActiveRecordRecord(mod)
vm.registerActiveRecordErrorsClass(mod)
vm.registerActiveRecordSchema(mod)
}

// registerActiveRecordErrors installs the ActiveRecord error tree:
// ActiveRecord::ActiveRecordError < StandardError, StatementInvalid (a failed
// query) and RecordInvalid (a failed validation) under it, mirroring the gem.
func (vm *VM) registerActiveRecordErrors(mod *RClass) {
std := vm.consts["StandardError"].(*RClass)
base := newClass("ActiveRecord::ActiveRecordError", std)
mod.consts["ActiveRecordError"] = base
vm.consts["ActiveRecord::ActiveRecordError"] = base

for _, name := range []string{"StatementInvalid", "RecordInvalid", "ConnectionNotEstablished"} {
c := newClass("ActiveRecord::"+name, base)
mod.consts[name] = c
vm.consts["ActiveRecord::"+name] = c
}
}

// registerActiveRecordBase installs ActiveRecord::Base and its
// establish_connection / connected? / connection class methods (the adapter seam).
func (vm *VM) registerActiveRecordBase(mod *RClass) {
base := newClass("ActiveRecord::Base", vm.cObject)
mod.consts["Base"] = base
vm.consts["ActiveRecord::Base"] = base

// establish_connection(database: ":memory:") / establish_connection(path)
// opens a sqlite3 database and installs it as the process adapter.
base.smethods["establish_connection"] = &Method{name: "establish_connection", owner: base, native: func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
path := activeRecordConnPath(args)
vm.arConnect(path)
return object.NilV
}}
base.smethods["connected?"] = &Method{name: "connected?", owner: base, native: func(vm *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value {
return object.Bool(vm.arAdapter != nil)
}}
// connection returns the underlying SQLite3::Database so raw #execute works.
base.smethods["connection"] = &Method{name: "connection", owner: base, native: func(vm *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value {
if vm.arAdapter == nil {
raise("ActiveRecord::ConnectionNotEstablished", "No connection pool for ActiveRecord::Base")
}
return &SQLite3Database{db: vm.arAdapter.db}
}}
}
113 changes: 113 additions & 0 deletions internal/vm/activerecord_adapter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright (c) the go-embedded-ruby/ruby authors
//
// SPDX-License-Identifier: BSD-3-Clause

package vm

import (
activerecord "github.com/go-ruby-activerecord/activerecord"
sqlite3 "github.com/go-ruby-sqlite3/sqlite3"

"github.com/go-embedded-ruby/ruby/internal/object"
)

// arSQLiteAdapter implements activerecord.Adapter over a live go-ruby-sqlite3
// database — the host seam that turns the deterministic SQL the activerecord core
// renders into real rows. It is the "adapter executor" wired to go-ruby-sqlite3
// so a Relation's #to_a / #count / #exists? / #pluck actually run.
type arSQLiteAdapter struct {
db *sqlite3.Database
}

// Execute runs a row-returning statement (a SELECT / existence probe) and yields
// the rows as ActiveRecord Rows keyed by column name.
func (a *arSQLiteAdapter) Execute(sql string) ([]activerecord.Row, error) {
rows, err := a.db.ExecuteHash(sql, nil)
if err != nil {
return nil, err
}
out := make([]activerecord.Row, len(rows))
for i, r := range rows {
out[i] = activerecord.Row(r)
}
return out, nil
}

// ExecuteDML runs an INSERT/UPDATE/DELETE and reports the affected-row count and
// last insert id the driver provides.
func (a *arSQLiteAdapter) ExecuteDML(sql string) (affected int64, lastInsertID int64, err error) {
if err := a.db.ExecuteBatch(sql, nil); err != nil {
return 0, 0, err
}
affected, _ = a.db.Changes()
lastInsertID, _ = a.db.LastInsertRowID()
return affected, lastInsertID, nil
}

// AdapterName reports the ActiveRecord adapter name, so the core picks the
// SQLite Dialect.
func (a *arSQLiteAdapter) AdapterName() string { return "sqlite3" }

// arConnect opens (or replaces) the process ActiveRecord connection at path,
// raising ActiveRecord::StatementInvalid on a failure to open.
func (vm *VM) arConnect(path string) {
db, err := sqlite3.New(path)
if err != nil {
raise("ActiveRecord::StatementInvalid", "%s", err.Error())
}
vm.arAdapter = &arSQLiteAdapter{db: db}
}

// arRequireAdapter returns the process adapter or raises
// ActiveRecord::ConnectionNotEstablished when no connection has been opened (the
// documented deferred case: SQL is always available via #to_sql, execution needs
// a connection).
func (vm *VM) arRequireAdapter() *arSQLiteAdapter {
if vm.arAdapter == nil {
raise("ActiveRecord::ConnectionNotEstablished", "No connection pool for ActiveRecord::Base; call establish_connection first")
}
return vm.arAdapter
}

// activeRecordConnPath reads the establish_connection argument: a Hash yields its
// :database / :adapter (":memory:" default), a String is the path directly.
func activeRecordConnPath(args []object.Value) string {
if len(args) == 0 {
return ":memory:"
}
switch v := args[0].(type) {
case *object.Hash:
if db, ok := v.Get(object.Symbol("database")); ok {
return arStr(db)
}
if db, ok := v.Get(object.NewString("database")); ok {
return arStr(db)
}
return ":memory:"
default:
return arStr(args[0])
}
}

// arValueToRuby maps a value scanned from the adapter (int64 / float64 / string /
// []byte / bool / nil) back into the rbgo object graph, mirroring the sqlite3
// binding's own scan mapping.
func arValueToRuby(v any) object.Value {
switch n := v.(type) {
case nil:
return object.NilV
case int64:
return object.Integer(n)
case int:
return object.Integer(int64(n))
case float64:
return object.Float(n)
case string:
return object.NewString(n)
case []byte:
return &object.String{B: n, Enc: "ASCII-8BIT"}
case bool:
return object.Bool(n)
}
return object.NilV
}
Loading
Loading