libsql_activerecord 0.0.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/active_record/connection_adapters/libsql_adapter.rb +269 -0
- data/lib/libsql-activerecord.rb +3 -0
- metadata +73 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bbe7dc7b7658ff6bc015aa2d5c83b12058f8ab13b838c183015248c24d2f78db
|
4
|
+
data.tar.gz: 1af1eda2128fbbac8924504897e7713731cd60848e9f2c2aadd308f808973684
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5ef0b717227f8a4e5d08bda341d7883c9fbe56a2de10e6707c7adfebaa2eaa701c18d0c6e8251c91fa6ba64f9c1a0a4a433b53e8dfca00caeeacd62123f490be
|
7
|
+
data.tar.gz: 2725820ec50d2bf270a7986a9a9c9818cd6b92116c1d33082cb210c711f120ee518088cdef75f1152a6a866f0cfaa52dced8599ba9d9b93a0928bdd9f8541917
|
@@ -0,0 +1,269 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_record'
|
4
|
+
require 'active_record/base'
|
5
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
6
|
+
require 'active_record/connection_adapters/sqlite3/database_statements'
|
7
|
+
require 'active_record/connection_adapters/sqlite3/schema_statements'
|
8
|
+
require 'active_record/connection_adapters/sqlite3/quoting'
|
9
|
+
|
10
|
+
require 'libsql'
|
11
|
+
|
12
|
+
module ActiveRecord
|
13
|
+
class Base # :nodoc:
|
14
|
+
class << self
|
15
|
+
def libsql_connection(config)
|
16
|
+
config = config.symbolize_keys
|
17
|
+
connection = ::Libsql::Database.new config
|
18
|
+
ConnectionAdapters::LibsqlAdapter.new(connection, logger, config)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module ConnectionAdapters # :nodoc:
|
24
|
+
if ActiveRecord.version >= Gem::Version.new('7.2')
|
25
|
+
register 'libsql', 'ActiveRecord::ConnectionAdapters::LibsqlAdapter',
|
26
|
+
'active_record/connection_adapters/libsql_adapter'
|
27
|
+
end
|
28
|
+
|
29
|
+
class LibsqlAdapter < AbstractAdapter # :nodoc:
|
30
|
+
ADAPTER_NAME = 'libSQL'
|
31
|
+
|
32
|
+
NATIVE_DATABASE_TYPES = {
|
33
|
+
primary_key: 'integer PRIMARY KEY AUTOINCREMENT NOT NULL',
|
34
|
+
string: { name: 'varchar' },
|
35
|
+
text: { name: 'text' },
|
36
|
+
integer: { name: 'integer' },
|
37
|
+
float: { name: 'float' },
|
38
|
+
decimal: { name: 'decimal' },
|
39
|
+
datetime: { name: 'datetime' },
|
40
|
+
time: { name: 'time' },
|
41
|
+
date: { name: 'date' },
|
42
|
+
binary: { name: 'blob' },
|
43
|
+
boolean: { name: 'boolean' },
|
44
|
+
json: { name: 'json' }
|
45
|
+
}.freeze
|
46
|
+
|
47
|
+
READ_QUERY = AbstractAdapter.build_read_query_regexp(:pragma)
|
48
|
+
private_constant :READ_QUERY
|
49
|
+
|
50
|
+
def write_query?(sql) # :nodoc:
|
51
|
+
!READ_QUERY.match?(sql)
|
52
|
+
end
|
53
|
+
|
54
|
+
def native_database_types # :nodoc:
|
55
|
+
NATIVE_DATABASE_TYPES
|
56
|
+
end
|
57
|
+
|
58
|
+
class << self
|
59
|
+
def new_client(config)
|
60
|
+
db = Libsql::Database.new(config || {})
|
61
|
+
db.connect
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def initialize(...)
|
66
|
+
super
|
67
|
+
@connection_parameters = @config.reject { |k| k == :adapter }
|
68
|
+
@connection_parameters[:url] = @connection_parameters[:host]
|
69
|
+
end
|
70
|
+
|
71
|
+
def connect
|
72
|
+
@raw_connection = self.class.new_client(@connection_parameters)
|
73
|
+
end
|
74
|
+
|
75
|
+
def reconnect
|
76
|
+
@raw_connection&.close
|
77
|
+
connect
|
78
|
+
end
|
79
|
+
|
80
|
+
def perform_query(
|
81
|
+
raw_connection, sql, binds, type_casted_binds, prepare:,
|
82
|
+
notification_payload:, batch: false
|
83
|
+
)
|
84
|
+
_ = prepare
|
85
|
+
_ = notification_payload
|
86
|
+
_ = binds
|
87
|
+
|
88
|
+
if batch
|
89
|
+
raw_connection.execute_batch(sql)
|
90
|
+
else
|
91
|
+
stmt = raw_connection.prepare(sql)
|
92
|
+
begin
|
93
|
+
result =
|
94
|
+
if stmt.column_count.zero?
|
95
|
+
@last_affected_rows = stmt.execute type_casted_binds
|
96
|
+
ActiveRecord::Result.empty
|
97
|
+
else
|
98
|
+
rows = stmt.query(type_casted_binds)
|
99
|
+
@last_affected_rows = nil
|
100
|
+
ActiveRecord::Result.new(rows.columns, rows.to_a.map(&:values))
|
101
|
+
end
|
102
|
+
ensure
|
103
|
+
stmt.close
|
104
|
+
end
|
105
|
+
end
|
106
|
+
verified!
|
107
|
+
|
108
|
+
result
|
109
|
+
end
|
110
|
+
|
111
|
+
def affected_rows(_result)
|
112
|
+
@last_affected_rows
|
113
|
+
end
|
114
|
+
|
115
|
+
def cast_result(result)
|
116
|
+
result
|
117
|
+
end
|
118
|
+
|
119
|
+
def quote_column_name(name)
|
120
|
+
%("#{name.to_s.gsub('"', '""')}").freeze
|
121
|
+
end
|
122
|
+
|
123
|
+
def quote_table_name(name)
|
124
|
+
%("#{name.to_s.gsub('"', '""').gsub('.', '"."')}").freeze
|
125
|
+
end
|
126
|
+
|
127
|
+
def column_definitions(table_name)
|
128
|
+
internal_exec_query("PRAGMA table_xinfo(#{quote_table_name(table_name)})", 'SCHEMA')
|
129
|
+
end
|
130
|
+
|
131
|
+
def data_source_sql(name = nil, type: nil)
|
132
|
+
scope = quoted_scope(name, type:)
|
133
|
+
scope[:type] ||= "'table','view'"
|
134
|
+
|
135
|
+
sql = +"SELECT name FROM pragma_table_list WHERE schema <> 'temp'"
|
136
|
+
sql << " AND name NOT IN ('sqlite_sequence', 'sqlite_schema')"
|
137
|
+
sql << " AND name = #{scope[:name]}" if scope[:name]
|
138
|
+
sql << " AND type IN (#{scope[:type]})"
|
139
|
+
sql
|
140
|
+
end
|
141
|
+
|
142
|
+
def quoted_scope(name = nil, type: nil)
|
143
|
+
type = {
|
144
|
+
'BASE_TABLE': "'table'",
|
145
|
+
'VIEW': "'view'",
|
146
|
+
'VIRTUAL TABLE': "'virtual'"
|
147
|
+
}[type]
|
148
|
+
|
149
|
+
scope = {}
|
150
|
+
scope[:name] = quote(name) if name
|
151
|
+
scope[:type] = type if type
|
152
|
+
scope
|
153
|
+
end
|
154
|
+
|
155
|
+
def extract_value_from_default(default)
|
156
|
+
case default
|
157
|
+
when /^null$/i then nil
|
158
|
+
when /^'([^|]*)'$/m then::Regexp.last_match(1).gsub("''", "'")
|
159
|
+
when /^"([^|]*)"$/m then ::Regexp.last_match(1).gsub('""', '"')
|
160
|
+
when /\A-?\d+(\.\d*)?\z/ then ::Regexp.last_match(0)
|
161
|
+
when /x'(.*)'/ then [::Regexp.last_match(1)].pack('H*')
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def extract_default_function(default_value, default)
|
166
|
+
default if default_function?(default_value, default)
|
167
|
+
end
|
168
|
+
|
169
|
+
def default_function?(default_value, default)
|
170
|
+
!default_value && /\w+\(.*\)|CURRENT_TIME|CURRENT_DATE|CURRENT_TIMESTAMP|\|\|/.match?(default)
|
171
|
+
end
|
172
|
+
|
173
|
+
def extract_generated_type(field)
|
174
|
+
case field['hidden']
|
175
|
+
when 2 then :virtual
|
176
|
+
when 3 then :stored
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def column_the_rowid?(field, column_definitions)
|
181
|
+
return false unless /integer/i.match?(field['type']) && field['pk'] == 1
|
182
|
+
|
183
|
+
column_definitions.one? { |c| c['pk'].positive? }
|
184
|
+
end
|
185
|
+
|
186
|
+
def new_column_from_field(_table_name, field, definitions)
|
187
|
+
default = field['dflt_value']
|
188
|
+
|
189
|
+
type_metadata = fetch_type_metadata(field['type'])
|
190
|
+
default_value = extract_value_from_default(default)
|
191
|
+
generated_type = extract_generated_type(field)
|
192
|
+
|
193
|
+
default_function =
|
194
|
+
if generated_type.present?
|
195
|
+
default
|
196
|
+
else
|
197
|
+
extract_default_function(default_value, default)
|
198
|
+
end
|
199
|
+
|
200
|
+
rowid = column_the_rowid?(field, definitions)
|
201
|
+
|
202
|
+
Column.new(
|
203
|
+
field['name'],
|
204
|
+
default_value,
|
205
|
+
type_metadata,
|
206
|
+
field['notnull'].to_i.zero?,
|
207
|
+
default_function,
|
208
|
+
collation: field['collation'],
|
209
|
+
auto_increment: field['auto_increment'],
|
210
|
+
rowid:,
|
211
|
+
generated_type:
|
212
|
+
)
|
213
|
+
end
|
214
|
+
|
215
|
+
def primary_keys(table_name) # :nodoc:
|
216
|
+
column_definitions(table_name)
|
217
|
+
.select { |f| f['pk'].positive? }
|
218
|
+
.sort_by { |f| f['pk'] }
|
219
|
+
.map { |f| f['name'] }
|
220
|
+
end
|
221
|
+
|
222
|
+
def indexes(table_name)
|
223
|
+
internal_exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", 'SCHEMA').filter_map do |row|
|
224
|
+
# Indexes SQLite creates implicitly for internal use start with "sqlite_".
|
225
|
+
# See https://www.sqlite.org/fileformat2.html#intschema
|
226
|
+
next if row['name'].start_with?('sqlite_')
|
227
|
+
|
228
|
+
index_sql = query_value(<<~SQL, 'SCHEMA')
|
229
|
+
SELECT sql
|
230
|
+
FROM sqlite_master
|
231
|
+
WHERE name = #{quote(row['name'])} AND type = 'index'
|
232
|
+
UNION ALL
|
233
|
+
SELECT sql
|
234
|
+
FROM sqlite_temp_master
|
235
|
+
WHERE name = #{quote(row['name'])} AND type = 'index'
|
236
|
+
SQL
|
237
|
+
|
238
|
+
%r{\bON\b\s*"?(\w+?)"?\s*\((?<expressions>.+?)\)(?:\s*WHERE\b\s*(?<where>.+))?(?:\s*/\*.*\*/)?\z}i =~ index_sql
|
239
|
+
|
240
|
+
columns = internal_exec_query("PRAGMA index_info(#{quote(row['name'])})", 'SCHEMA').map do |col|
|
241
|
+
col['name']
|
242
|
+
end
|
243
|
+
|
244
|
+
where = where.sub(%r{\s*/\*.*\*/\z}, '') if where
|
245
|
+
orders = {}
|
246
|
+
|
247
|
+
if columns.any?(&:nil?) # index created with an expression
|
248
|
+
columns = expressions
|
249
|
+
elsif index_sql
|
250
|
+
# Add info on sort order for columns (only desc order is explicitly specified,
|
251
|
+
# asc is the default)
|
252
|
+
index_sql.scan(/"(\w+)" DESC/).flatten.each do |order_column|
|
253
|
+
orders[order_column] = :desc
|
254
|
+
end # index_sql can be null in case of primary key indexes
|
255
|
+
end
|
256
|
+
|
257
|
+
IndexDefinition.new(
|
258
|
+
table_name,
|
259
|
+
row['name'],
|
260
|
+
row['unique'] != 0,
|
261
|
+
columns,
|
262
|
+
where:,
|
263
|
+
orders:
|
264
|
+
)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: libsql_activerecord
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Levy A.
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 1980-01-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '8.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '8.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: turso_libsql
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.1'
|
41
|
+
description:
|
42
|
+
email:
|
43
|
+
- levyddsa@gmail.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- lib/active_record/connection_adapters/libsql_adapter.rb
|
49
|
+
- lib/libsql-activerecord.rb
|
50
|
+
homepage: https://rubygems.org/gems/libsql_activerecord
|
51
|
+
licenses:
|
52
|
+
- MIT
|
53
|
+
metadata: {}
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options: []
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '3.3'
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
requirements: []
|
69
|
+
rubygems_version: 3.5.16
|
70
|
+
signing_key:
|
71
|
+
specification_version: 4
|
72
|
+
summary: libSQL ActiveRecord Adapter
|
73
|
+
test_files: []
|