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 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
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record/connection_adapters/libsql_adapter'
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: []