| 68 | | class SQLiteColumn < Column #:nodoc: |
|---|
| 69 | | class << self |
|---|
| 70 | | def string_to_binary(value) |
|---|
| 71 | | value.gsub(/\0|\%/n) do |b| |
|---|
| 72 | | case b |
|---|
| 73 | | when "\0" then "%00" |
|---|
| 74 | | when "%" then "%25" |
|---|
| 75 | | end |
|---|
| 76 | | end |
|---|
| 77 | | end |
|---|
| 78 | | |
|---|
| 79 | | def binary_to_string(value) |
|---|
| 80 | | value.gsub(/%00|%25/n) do |b| |
|---|
| 81 | | case b |
|---|
| 82 | | when "%00" then "\0" |
|---|
| 83 | | when "%25" then "%" |
|---|
| 84 | | end |
|---|
| 85 | | end |
|---|
| 86 | | end |
|---|
| 87 | | end |
|---|
| 88 | | end |
|---|
| 89 | | |
|---|
| 90 | | # The SQLite adapter works with both the 2.x and 3.x series of SQLite with the sqlite-ruby drivers (available both as gems and |
|---|
| 91 | | # from http://rubyforge.org/projects/sqlite-ruby/). |
|---|
| 92 | | # |
|---|
| 93 | | # Options: |
|---|
| 94 | | # |
|---|
| 95 | | # * <tt>:database</tt> -- Path to the database file. |
|---|
| 96 | | class SQLiteAdapter < AbstractAdapter |
|---|
| 97 | | def adapter_name #:nodoc: |
|---|
| 98 | | 'SQLite' |
|---|
| 99 | | end |
|---|
| 100 | | |
|---|
| 101 | | def supports_migrations? #:nodoc: |
|---|
| 102 | | true |
|---|
| 103 | | end |
|---|
| 104 | | |
|---|
| 105 | | def requires_reloading? |
|---|
| 106 | | true |
|---|
| 107 | | end |
|---|
| 108 | | |
|---|
| 109 | | def disconnect! |
|---|
| 110 | | super |
|---|
| 111 | | @connection.close rescue nil |
|---|
| 112 | | end |
|---|
| 113 | | |
|---|
| 114 | | def supports_count_distinct? #:nodoc: |
|---|
| 115 | | sqlite_version >= '3.2.6' |
|---|
| 116 | | end |
|---|
| 117 | | |
|---|
| 118 | | def supports_autoincrement? #:nodoc: |
|---|
| 119 | | sqlite_version >= '3.1.0' |
|---|
| 120 | | end |
|---|
| 121 | | |
|---|
| 122 | | def native_database_types #:nodoc: |
|---|
| 123 | | { |
|---|
| 124 | | :primary_key => default_primary_key_type, |
|---|
| 125 | | :string => { :name => "varchar", :limit => 255 }, |
|---|
| 126 | | :text => { :name => "text" }, |
|---|
| 127 | | :integer => { :name => "integer" }, |
|---|
| 128 | | :float => { :name => "float" }, |
|---|
| 129 | | :decimal => { :name => "decimal" }, |
|---|
| 130 | | :datetime => { :name => "datetime" }, |
|---|
| 131 | | :timestamp => { :name => "datetime" }, |
|---|
| 132 | | :time => { :name => "datetime" }, |
|---|
| 133 | | :date => { :name => "date" }, |
|---|
| 134 | | :binary => { :name => "blob" }, |
|---|
| 135 | | :boolean => { :name => "boolean" } |
|---|
| 136 | | } |
|---|
| 137 | | end |
|---|
| 138 | | |
|---|
| 139 | | |
|---|
| 140 | | # QUOTING ================================================== |
|---|
| 141 | | |
|---|
| 142 | | def quote_string(s) #:nodoc: |
|---|
| 143 | | @connection.class.quote(s) |
|---|
| 144 | | end |
|---|
| 145 | | |
|---|
| 146 | | def quote_column_name(name) #:nodoc: |
|---|
| 147 | | %Q("#{name}") |
|---|
| 148 | | end |
|---|
| 149 | | |
|---|
| 150 | | |
|---|
| 151 | | # DATABASE STATEMENTS ====================================== |
|---|
| 152 | | |
|---|
| 153 | | def execute(sql, name = nil) #:nodoc: |
|---|
| 154 | | catch_schema_changes { log(sql, name) { @connection.execute(sql) } } |
|---|
| 155 | | end |
|---|
| 156 | | |
|---|
| 157 | | def update(sql, name = nil) #:nodoc: |
|---|
| 158 | | execute(sql, name) |
|---|
| 159 | | @connection.changes |
|---|
| 160 | | end |
|---|
| 161 | | |
|---|
| 162 | | def delete(sql, name = nil) #:nodoc: |
|---|
| 163 | | sql += " WHERE 1=1" unless sql =~ /WHERE/i |
|---|
| 164 | | execute(sql, name) |
|---|
| 165 | | @connection.changes |
|---|
| 166 | | end |
|---|
| 167 | | |
|---|
| 168 | | def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: |
|---|
| 169 | | execute(sql, name = nil) |
|---|
| 170 | | id_value || @connection.last_insert_row_id |
|---|
| 171 | | end |
|---|
| 172 | | |
|---|
| 173 | | def select_all(sql, name = nil) #:nodoc: |
|---|
| 174 | | execute(sql, name).map do |row| |
|---|
| 175 | | record = {} |
|---|
| 176 | | row.each_key do |key| |
|---|
| 177 | | if key.is_a?(String) |
|---|
| 178 | | record[key.sub(/^\w+\./, '')] = row[key] |
|---|
| 179 | | end |
|---|
| 180 | | end |
|---|
| 181 | | record |
|---|
| 182 | | end |
|---|
| 183 | | end |
|---|
| 184 | | |
|---|
| 185 | | def select_one(sql, name = nil) #:nodoc: |
|---|
| 186 | | result = select_all(sql, name) |
|---|
| 187 | | result.nil? ? nil : result.first |
|---|
| 188 | | end |
|---|
| 189 | | |
|---|
| 190 | | |
|---|
| 191 | | def begin_db_transaction #:nodoc: |
|---|
| 192 | | catch_schema_changes { @connection.transaction } |
|---|
| 193 | | end |
|---|
| 194 | | |
|---|
| 195 | | def commit_db_transaction #:nodoc: |
|---|
| 196 | | catch_schema_changes { @connection.commit } |
|---|
| 197 | | end |
|---|
| 198 | | |
|---|
| 199 | | def rollback_db_transaction #:nodoc: |
|---|
| 200 | | catch_schema_changes { @connection.rollback } |
|---|
| 201 | | end |
|---|
| 202 | | |
|---|
| 203 | | |
|---|
| 204 | | # SELECT ... FOR UPDATE is redundant since the table is locked. |
|---|
| 205 | | def add_lock!(sql, options) #:nodoc: |
|---|
| 206 | | sql |
|---|
| 207 | | end |
|---|
| 208 | | |
|---|
| 209 | | |
|---|
| 210 | | # SCHEMA STATEMENTS ======================================== |
|---|
| 211 | | |
|---|
| 212 | | def tables(name = nil) #:nodoc: |
|---|
| 213 | | sql = <<-SQL |
|---|
| 214 | | SELECT name |
|---|
| 215 | | FROM sqlite_master |
|---|
| 216 | | WHERE type = 'table' AND NOT name = 'sqlite_sequence' |
|---|
| 217 | | SQL |
|---|
| 218 | | |
|---|
| 219 | | execute(sql, name).map do |row| |
|---|
| 220 | | row[0] |
|---|
| 221 | | end |
|---|
| 222 | | end |
|---|
| 223 | | |
|---|
| 224 | | def columns(table_name, name = nil) #:nodoc: |
|---|
| 225 | | table_structure(table_name).map do |field| |
|---|
| 226 | | SQLiteColumn.new(field['name'], field['dflt_value'], field['type'], field['notnull'] == "0") |
|---|
| 227 | | end |
|---|
| 228 | | end |
|---|
| 229 | | |
|---|
| 230 | | def indexes(table_name, name = nil) #:nodoc: |
|---|
| 231 | | execute("PRAGMA index_list(#{table_name})", name).map do |row| |
|---|
| 232 | | index = IndexDefinition.new(table_name, row['name']) |
|---|
| 233 | | index.unique = row['unique'] != '0' |
|---|
| 234 | | index.columns = execute("PRAGMA index_info('#{index.name}')").map { |col| col['name'] } |
|---|
| 235 | | index |
|---|
| 236 | | end |
|---|
| 237 | | end |
|---|
| 238 | | |
|---|
| 239 | | def primary_key(table_name) #:nodoc: |
|---|
| 240 | | column = table_structure(table_name).find {|field| field['pk'].to_i == 1} |
|---|
| 241 | | column ? column['name'] : nil |
|---|
| 242 | | end |
|---|
| 243 | | |
|---|
| 244 | | def remove_index(table_name, options={}) #:nodoc: |
|---|
| 245 | | execute "DROP INDEX #{quote_column_name(index_name(table_name, options))}" |
|---|
| 246 | | end |
|---|
| 247 | | |
|---|
| 248 | | def rename_table(name, new_name) |
|---|
| 249 | | execute "ALTER TABLE #{name} RENAME TO #{new_name}" |
|---|
| 250 | | end |
|---|
| 251 | | |
|---|
| 252 | | def add_column(table_name, column_name, type, options = {}) #:nodoc: |
|---|
| 253 | | super(table_name, column_name, type, options) |
|---|
| 254 | | # See last paragraph on http://www.sqlite.org/lang_altertable.html |
|---|
| 255 | | execute "VACUUM" |
|---|
| 256 | | end |
|---|
| 257 | | |
|---|
| 258 | | def remove_column(table_name, column_name) #:nodoc: |
|---|
| 259 | | alter_table(table_name) do |definition| |
|---|
| 260 | | definition.columns.delete(definition[column_name]) |
|---|
| 261 | | end |
|---|
| 262 | | end |
|---|
| 263 | | |
|---|
| 264 | | def change_column_default(table_name, column_name, default) #:nodoc: |
|---|
| 265 | | alter_table(table_name) do |definition| |
|---|
| 266 | | definition[column_name].default = default |
|---|
| 267 | | end |
|---|
| 268 | | end |
|---|
| 269 | | |
|---|
| 270 | | def change_column(table_name, column_name, type, options = {}) #:nodoc: |
|---|
| 271 | | alter_table(table_name) do |definition| |
|---|
| 272 | | include_default = options_include_default?(options) |
|---|
| 273 | | definition[column_name].instance_eval do |
|---|
| 274 | | self.type = type |
|---|
| 275 | | self.limit = options[:limit] if options.include?(:limit) |
|---|
| 276 | | self.default = options[:default] if include_default |
|---|
| 277 | | end |
|---|
| 278 | | end |
|---|
| 279 | | end |
|---|
| 280 | | |
|---|
| 281 | | def rename_column(table_name, column_name, new_column_name) #:nodoc: |
|---|
| 282 | | alter_table(table_name, :rename => {column_name => new_column_name}) |
|---|
| 283 | | end |
|---|
| 284 | | |
|---|
| 285 | | |
|---|
| 286 | | protected |
|---|
| 287 | | def table_structure(table_name) |
|---|
| 288 | | returning structure = execute("PRAGMA table_info(#{table_name})") do |
|---|
| 289 | | raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty? |
|---|
| 290 | | end |
|---|
| 291 | | end |
|---|
| 292 | | |
|---|
| 293 | | def alter_table(table_name, options = {}) #:nodoc: |
|---|
| 294 | | altered_table_name = "altered_#{table_name}" |
|---|
| 295 | | caller = lambda {|definition| yield definition if block_given?} |
|---|
| 296 | | |
|---|
| 297 | | transaction do |
|---|
| 298 | | move_table(table_name, altered_table_name, |
|---|
| 299 | | options.merge(:temporary => true)) |
|---|
| 300 | | move_table(altered_table_name, table_name, &caller) |
|---|
| 301 | | end |
|---|
| 302 | | end |
|---|
| 303 | | |
|---|
| 304 | | def move_table(from, to, options = {}, &block) #:nodoc: |
|---|
| 305 | | copy_table(from, to, options, &block) |
|---|
| 306 | | drop_table(from) |
|---|
| 307 | | end |
|---|
| 308 | | |
|---|
| 309 | | def copy_table(from, to, options = {}) #:nodoc: |
|---|
| 310 | | create_table(to, options) do |@definition| |
|---|
| 311 | | columns(from).each do |column| |
|---|
| 312 | | column_name = options[:rename] ? |
|---|
| 313 | | (options[:rename][column.name] || |
|---|
| 314 | | options[:rename][column.name.to_sym] || |
|---|
| 315 | | column.name) : column.name |
|---|
| 316 | | |
|---|
| 317 | | @definition.column(column_name, column.type, |
|---|
| 318 | | :limit => column.limit, :default => column.default, |
|---|
| 319 | | :null => column.null) |
|---|
| 320 | | end |
|---|
| 321 | | @definition.primary_key(primary_key(from)) |
|---|
| 322 | | yield @definition if block_given? |
|---|
| 323 | | end |
|---|
| 324 | | |
|---|
| 325 | | copy_table_indexes(from, to) |
|---|
| 326 | | copy_table_contents(from, to, |
|---|
| 327 | | @definition.columns.map {|column| column.name}, |
|---|
| 328 | | options[:rename] || {}) |
|---|
| 329 | | end |
|---|
| 330 | | |
|---|
| 331 | | def copy_table_indexes(from, to) #:nodoc: |
|---|
| 332 | | indexes(from).each do |index| |
|---|
| 333 | | name = index.name |
|---|
| 334 | | if to == "altered_#{from}" |
|---|
| 335 | | name = "temp_#{name}" |
|---|
| 336 | | elsif from == "altered_#{to}" |
|---|
| 337 | | name = name[5..-1] |
|---|
| 338 | | end |
|---|
| 339 | | |
|---|
| 340 | | # index name can't be the same |
|---|
| 341 | | opts = { :name => name.gsub(/_(#{from})_/, "_#{to}_") } |
|---|
| 342 | | opts[:unique] = true if index.unique |
|---|
| 343 | | add_index(to, index.columns, opts) |
|---|
| 344 | | end |
|---|
| 345 | | end |
|---|
| 346 | | |
|---|
| 347 | | def copy_table_contents(from, to, columns, rename = {}) #:nodoc: |
|---|
| 348 | | column_mappings = Hash[*columns.map {|name| [name, name]}.flatten] |
|---|
| 349 | | rename.inject(column_mappings) {|map, a| map[a.last] = a.first; map} |
|---|
| 350 | | from_columns = columns(from).collect {|col| col.name} |
|---|
| 351 | | columns = columns.find_all{|col| from_columns.include?(column_mappings[col])} |
|---|
| 352 | | @connection.execute "SELECT * FROM #{from}" do |row| |
|---|
| 353 | | sql = "INSERT INTO #{to} ("+columns*','+") VALUES (" |
|---|
| 354 | | sql << columns.map {|col| quote row[column_mappings[col]]} * ', ' |
|---|
| 355 | | sql << ')' |
|---|
| 356 | | @connection.execute sql |
|---|
| 357 | | end |
|---|
| 358 | | end |
|---|
| 359 | | |
|---|
| 360 | | def catch_schema_changes |
|---|
| 361 | | return yield |
|---|
| 362 | | rescue ActiveRecord::StatementInvalid => exception |
|---|
| 363 | | if exception.message =~ /database schema has changed/ |
|---|
| 364 | | reconnect! |
|---|
| 365 | | retry |
|---|
| 366 | | else |
|---|
| 367 | | raise |
|---|
| 368 | | end |
|---|
| 369 | | end |
|---|
| 370 | | |
|---|
| 371 | | def sqlite_version |
|---|
| 372 | | @sqlite_version ||= select_value('select sqlite_version(*)') |
|---|
| 373 | | end |
|---|
| 374 | | |
|---|
| 375 | | def default_primary_key_type |
|---|
| 376 | | if supports_autoincrement? |
|---|
| 377 | | 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL'.freeze |
|---|
| 378 | | else |
|---|
| 379 | | 'INTEGER PRIMARY KEY NOT NULL'.freeze |
|---|
| 380 | | end |
|---|
| 381 | | end |
|---|
| 382 | | end |
|---|
| 383 | | |
|---|