activerecord 1.11.1 → 1.12.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (102) hide show
  1. data/CHANGELOG +198 -0
  2. data/lib/active_record.rb +19 -14
  3. data/lib/active_record/acts/list.rb +8 -6
  4. data/lib/active_record/acts/tree.rb +33 -10
  5. data/lib/active_record/aggregations.rb +1 -7
  6. data/lib/active_record/associations.rb +151 -82
  7. data/lib/active_record/associations/association_collection.rb +25 -0
  8. data/lib/active_record/associations/association_proxy.rb +9 -8
  9. data/lib/active_record/associations/belongs_to_association.rb +19 -5
  10. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +44 -69
  11. data/lib/active_record/associations/has_many_association.rb +6 -14
  12. data/lib/active_record/associations/has_one_association.rb +5 -3
  13. data/lib/active_record/base.rb +344 -130
  14. data/lib/active_record/callbacks.rb +2 -2
  15. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +128 -0
  16. data/lib/active_record/connection_adapters/abstract/database_statements.rb +104 -0
  17. data/lib/active_record/connection_adapters/abstract/quoting.rb +51 -0
  18. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +249 -0
  19. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +245 -0
  20. data/lib/active_record/connection_adapters/abstract_adapter.rb +29 -464
  21. data/lib/active_record/connection_adapters/db2_adapter.rb +40 -10
  22. data/lib/active_record/connection_adapters/mysql_adapter.rb +131 -60
  23. data/lib/active_record/connection_adapters/oci_adapter.rb +106 -26
  24. data/lib/active_record/connection_adapters/postgresql_adapter.rb +211 -62
  25. data/lib/active_record/connection_adapters/sqlite_adapter.rb +193 -44
  26. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +24 -15
  27. data/lib/active_record/fixtures.rb +47 -24
  28. data/lib/active_record/migration.rb +34 -5
  29. data/lib/active_record/observer.rb +32 -2
  30. data/lib/active_record/query_cache.rb +12 -11
  31. data/lib/active_record/schema.rb +58 -0
  32. data/lib/active_record/schema_dumper.rb +84 -0
  33. data/lib/active_record/transactions.rb +1 -3
  34. data/lib/active_record/validations.rb +40 -26
  35. data/lib/active_record/vendor/mysql.rb +6 -0
  36. data/lib/active_record/version.rb +9 -0
  37. data/rakefile +5 -16
  38. data/test/abstract_unit.rb +6 -11
  39. data/test/adapter_test.rb +58 -0
  40. data/test/ar_schema_test.rb +33 -0
  41. data/test/association_callbacks_test.rb +14 -0
  42. data/test/associations_go_eager_test.rb +56 -14
  43. data/test/associations_test.rb +245 -25
  44. data/test/base_test.rb +205 -34
  45. data/test/binary_test.rb +25 -42
  46. data/test/callbacks_test.rb +75 -0
  47. data/test/conditions_scoping_test.rb +136 -0
  48. data/test/connections/native_mysql/connection.rb +0 -4
  49. data/test/connections/native_sqlite3/in_memory_connection.rb +17 -0
  50. data/test/copy_table_sqlite.rb +64 -0
  51. data/test/deprecated_associations_test.rb +7 -6
  52. data/test/deprecated_finder_test.rb +3 -3
  53. data/test/finder_test.rb +33 -3
  54. data/test/fixtures/accounts.yml +5 -0
  55. data/test/fixtures/categories_ordered.yml +7 -0
  56. data/test/fixtures/category.rb +11 -1
  57. data/test/fixtures/comment.rb +22 -2
  58. data/test/fixtures/comments.yml +6 -0
  59. data/test/fixtures/companies.yml +15 -0
  60. data/test/fixtures/company.rb +24 -1
  61. data/test/fixtures/db_definitions/db2.drop.sql +5 -1
  62. data/test/fixtures/db_definitions/db2.sql +15 -1
  63. data/test/fixtures/db_definitions/mysql.drop.sql +2 -0
  64. data/test/fixtures/db_definitions/mysql.sql +17 -2
  65. data/test/fixtures/db_definitions/oci.drop.sql +37 -5
  66. data/test/fixtures/db_definitions/oci.sql +47 -4
  67. data/test/fixtures/db_definitions/oci2.drop.sql +1 -1
  68. data/test/fixtures/db_definitions/oci2.sql +2 -2
  69. data/test/fixtures/db_definitions/postgresql.drop.sql +4 -0
  70. data/test/fixtures/db_definitions/postgresql.sql +33 -4
  71. data/test/fixtures/db_definitions/sqlite.drop.sql +2 -0
  72. data/test/fixtures/db_definitions/sqlite.sql +16 -2
  73. data/test/fixtures/db_definitions/sqlserver.drop.sql +2 -0
  74. data/test/fixtures/db_definitions/sqlserver.sql +16 -2
  75. data/test/fixtures/developer.rb +1 -1
  76. data/test/fixtures/flowers.jpg +0 -0
  77. data/test/fixtures/keyboard.rb +3 -0
  78. data/test/fixtures/mixins.yml +11 -1
  79. data/test/fixtures/order.rb +4 -0
  80. data/test/fixtures/post.rb +4 -0
  81. data/test/fixtures/posts.yml +7 -0
  82. data/test/fixtures/project.rb +1 -0
  83. data/test/fixtures/subject.rb +4 -0
  84. data/test/fixtures/subscriber.rb +2 -4
  85. data/test/fixtures/topics.yml +2 -2
  86. data/test/fixtures_test.rb +79 -7
  87. data/test/inheritance_test.rb +2 -2
  88. data/test/lifecycle_test.rb +14 -6
  89. data/test/migration_test.rb +164 -6
  90. data/test/mixin_test.rb +78 -2
  91. data/test/pk_test.rb +25 -1
  92. data/test/readonly_test.rb +31 -0
  93. data/test/reflection_test.rb +4 -1
  94. data/test/schema_dumper_test.rb +19 -0
  95. data/test/schema_test_postgresql.rb +3 -2
  96. data/test/synonym_test_oci.rb +17 -0
  97. data/test/threaded_connections_test.rb +2 -1
  98. data/test/transactions_test.rb +109 -10
  99. data/test/validations_test.rb +70 -42
  100. metadata +25 -5
  101. data/test/fixtures/associations.png +0 -0
  102. data/test/thread_safety_test.rb +0 -36
@@ -1,24 +1,12 @@
1
-
2
- # postgresql_adaptor.rb
3
- # author: Luke Holden <lholden@cablelan.net>
4
- # notes: Currently this adaptor does not pass the test_zero_date_fields
5
- # and test_zero_datetime_fields unit tests in the BasicsTest test
6
- # group.
7
- #
8
- # This is due to the fact that, in postgresql you can not have a
9
- # totally zero timestamp. Instead null/nil should be used to
10
- # represent no value.
11
- #
12
-
13
1
  require 'active_record/connection_adapters/abstract_adapter'
14
- require 'parsedate'
15
2
 
16
3
  module ActiveRecord
17
4
  class Base
18
5
  # Establishes a connection to the database that's used by all Active Record objects
19
6
  def self.postgresql_connection(config) # :nodoc:
20
7
  require_library_or_gem 'postgres' unless self.class.const_defined?(:PGconn)
21
- symbolize_strings_in_hash(config)
8
+
9
+ config = config.symbolize_keys
22
10
  host = config[:host]
23
11
  port = config[:port] || 5432 unless host.nil?
24
12
  username = config[:username].to_s
@@ -60,6 +48,10 @@ module ActiveRecord
60
48
  # * <tt>:encoding</tt> -- An optional client encoding that is using in a SET client_encoding TO <encoding> call on connection.
61
49
  # * <tt>:min_messages</tt> -- An optional client min messages that is using in a SET client_min_messages TO <min_messages> call on connection.
62
50
  class PostgreSQLAdapter < AbstractAdapter
51
+ def adapter_name
52
+ 'PostgreSQL'
53
+ end
54
+
63
55
  def native_database_types
64
56
  {
65
57
  :primary_key => "serial primary key",
@@ -72,7 +64,7 @@ module ActiveRecord
72
64
  :time => { :name => "timestamp" },
73
65
  :date => { :name => "date" },
74
66
  :binary => { :name => "bytea" },
75
- :boolean => { :name => "boolean"}
67
+ :boolean => { :name => "boolean" }
76
68
  }
77
69
  end
78
70
 
@@ -80,99 +72,218 @@ module ActiveRecord
80
72
  true
81
73
  end
82
74
 
83
- def select_all(sql, name = nil)
84
- select(sql, name)
75
+
76
+ # QUOTING ==================================================
77
+
78
+ def quote(value, column = nil)
79
+ if value.kind_of?(String) && column && column.type == :binary
80
+ "'#{escape_bytea(value)}'"
81
+ else
82
+ super
83
+ end
85
84
  end
86
85
 
87
- def select_one(sql, name = nil)
88
- result = select(sql, name)
89
- result.nil? ? nil : result.first
86
+ def quote_column_name(name)
87
+ %("#{name}")
90
88
  end
91
89
 
92
- def columns(table_name, name = nil)
93
- column_definitions(table_name).collect do |name, type, default|
94
- Column.new(name, default_value(default), translate_field_type(type))
95
- end
90
+
91
+ # DATABASE STATEMENTS ======================================
92
+
93
+ def select_all(sql, name = nil) #:nodoc:
94
+ select(sql, name)
95
+ end
96
+
97
+ def select_one(sql, name = nil) #:nodoc:
98
+ result = select(sql, name)
99
+ result.first if result
96
100
  end
97
101
 
98
- def insert(sql, name = nil, pk = nil, id_value = nil)
102
+ def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
99
103
  execute(sql, name)
100
104
  table = sql.split(" ", 4)[2]
101
- return id_value || last_insert_id(table, pk)
105
+ id_value || last_insert_id(table, sequence_name)
102
106
  end
103
107
 
104
- def query(sql, name = nil)
108
+ def query(sql, name = nil) #:nodoc:
105
109
  log(sql, name) { @connection.query(sql) }
106
110
  end
107
111
 
108
- def execute(sql, name = nil)
112
+ def execute(sql, name = nil) #:nodoc:
109
113
  log(sql, name) { @connection.exec(sql) }
110
114
  end
111
115
 
112
- def update(sql, name = nil)
116
+ def update(sql, name = nil) #:nodoc:
113
117
  execute(sql, name).cmdtuples
114
118
  end
115
119
 
116
- alias_method :delete, :update
120
+ alias_method :delete, :update #:nodoc:
117
121
 
118
- def begin_db_transaction() execute "BEGIN" end
119
- def commit_db_transaction() execute "COMMIT" end
120
- def rollback_db_transaction() execute "ROLLBACK" end
121
122
 
122
- def quote(value, column = nil)
123
- if value.class == String && column && column.type == :binary
124
- quote_bytea(value)
125
- else
126
- super
127
- end
123
+ def begin_db_transaction #:nodoc:
124
+ execute "BEGIN"
128
125
  end
129
126
 
130
- def quote_column_name(name)
131
- %("#{name}")
127
+ def commit_db_transaction #:nodoc:
128
+ execute "COMMIT"
129
+ end
130
+
131
+ def rollback_db_transaction #:nodoc:
132
+ execute "ROLLBACK"
132
133
  end
133
134
 
134
- def adapter_name
135
- 'PostgreSQL'
135
+
136
+ # SCHEMA STATEMENTS ========================================
137
+
138
+ # Return the list of all tables in the schema search path.
139
+ def tables(name = nil) #:nodoc:
140
+ schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',')
141
+ query(<<-SQL, name).map { |row| row[0] }
142
+ SELECT tablename
143
+ FROM pg_tables
144
+ WHERE schemaname IN (#{schemas})
145
+ SQL
146
+ end
147
+
148
+ def indexes(table_name, name = nil) #:nodoc:
149
+ result = query(<<-SQL, name)
150
+ SELECT i.relname, d.indisunique, a.attname
151
+ FROM pg_class t, pg_class i, pg_index d, pg_attribute a
152
+ WHERE i.relkind = 'i'
153
+ AND d.indexrelid = i.oid
154
+ AND d.indisprimary = 'f'
155
+ AND t.oid = d.indrelid
156
+ AND t.relname = '#{table_name}'
157
+ AND a.attrelid = t.oid
158
+ AND ( d.indkey[0]=a.attnum OR d.indkey[1]=a.attnum
159
+ OR d.indkey[2]=a.attnum OR d.indkey[3]=a.attnum
160
+ OR d.indkey[4]=a.attnum OR d.indkey[5]=a.attnum
161
+ OR d.indkey[6]=a.attnum OR d.indkey[7]=a.attnum
162
+ OR d.indkey[8]=a.attnum OR d.indkey[9]=a.attnum )
163
+ ORDER BY i.relname
164
+ SQL
165
+
166
+ current_index = nil
167
+ indexes = []
168
+
169
+ result.each do |row|
170
+ if current_index != row[0]
171
+ indexes << IndexDefinition.new(table_name, row[0], row[1] == "t", [])
172
+ current_index = row[0]
173
+ end
174
+
175
+ indexes.last.columns << row[2]
176
+ end
177
+
178
+ indexes
136
179
  end
137
180
 
181
+ def columns(table_name, name = nil) #:nodoc:
182
+ column_definitions(table_name).collect do |name, type, default, notnull|
183
+ Column.new(name, default_value(default), translate_field_type(type),
184
+ notnull == "f")
185
+ end
186
+ end
138
187
 
139
188
  # Set the schema search path to a string of comma-separated schema names.
140
189
  # Names beginning with $ are quoted (e.g. $user => '$user')
141
190
  # See http://www.postgresql.org/docs/8.0/interactive/ddl-schemas.html
142
- def schema_search_path=(schema_csv)
191
+ def schema_search_path=(schema_csv) #:nodoc:
143
192
  if schema_csv
144
193
  execute "SET search_path TO #{schema_csv}"
145
194
  @schema_search_path = nil
146
195
  end
147
196
  end
148
197
 
149
- def schema_search_path
198
+ def schema_search_path #:nodoc:
150
199
  @schema_search_path ||= query('SHOW search_path')[0][0]
151
200
  end
201
+
202
+ def default_sequence_name(table_name, pk = 'id')
203
+ "#{table_name}_#{pk}_seq"
204
+ end
205
+
206
+ # Set the sequence to the max value of the table's pk.
207
+ def reset_pk_sequence!(table)
208
+ pk, sequence = pk_and_sequence_for(table)
209
+ if pk and sequence
210
+ select_value <<-end_sql, 'Reset sequence'
211
+ SELECT setval('#{sequence}', (SELECT COALESCE(MAX(#{pk})+(SELECT increment_by FROM #{sequence}), (SELECT min_value FROM #{sequence})) FROM #{table}), false)
212
+ end_sql
213
+ end
214
+ end
215
+
216
+ # Find a table's primary key and sequence.
217
+ def pk_and_sequence_for(table)
218
+ execute(<<-end_sql, 'Find pk sequence')[0]
219
+ SELECT attr.attname, (name.nspname || '.' || seq.relname)
220
+ FROM pg_class seq,
221
+ pg_attribute attr,
222
+ pg_depend dep,
223
+ pg_namespace name,
224
+ pg_constraint cons
225
+ WHERE seq.oid = dep.objid
226
+ AND seq.relnamespace = name.oid
227
+ AND seq.relkind = 'S'
228
+ AND attr.attrelid = dep.refobjid
229
+ AND attr.attnum = dep.refobjsubid
230
+ AND attr.attrelid = cons.conrelid
231
+ AND attr.attnum = cons.conkey[1]
232
+ AND cons.contype = 'p'
233
+ AND dep.refobjid = '#{table}'::regclass
234
+ end_sql
235
+ rescue
236
+ nil
237
+ end
238
+
239
+
240
+ def rename_table(name, new_name)
241
+ execute "ALTER TABLE #{name} RENAME TO #{new_name}"
242
+ end
152
243
 
153
- def change_column(table_name, column_name, type, options = {})
244
+ def add_column(table_name, column_name, type, options = {})
245
+ native_type = native_database_types[type]
246
+ sql_commands = ["ALTER TABLE #{table_name} ADD #{column_name} #{type_to_sql(type, options[:limit])}"]
247
+ if options[:default]
248
+ sql_commands << "ALTER TABLE #{table_name} ALTER #{column_name} SET DEFAULT '#{options[:default]}'"
249
+ end
250
+ if options[:null] == false
251
+ sql_commands << "ALTER TABLE #{table_name} ALTER #{column_name} SET NOT NULL"
252
+ end
253
+ sql_commands.each { |cmd| execute(cmd) }
254
+ end
255
+
256
+ def change_column(table_name, column_name, type, options = {}) #:nodoc:
154
257
  execute = "ALTER TABLE #{table_name} ALTER #{column_name} TYPE #{type}"
155
258
  change_column_default(table_name, column_name, options[:default]) unless options[:default].nil?
156
259
  end
157
260
 
158
- def change_column_default(table_name, column_name, default)
261
+ def change_column_default(table_name, column_name, default) #:nodoc:
159
262
  execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} SET DEFAULT '#{default}'"
160
263
  end
161
264
 
162
- def rename_column(table_name, column_name, new_column_name)
265
+ def rename_column(table_name, column_name, new_column_name) #:nodoc:
163
266
  execute "ALTER TABLE #{table_name} RENAME COLUMN #{column_name} TO #{new_column_name}"
164
267
  end
165
268
 
166
- def remove_index(table_name, column_name)
167
- execute "DROP INDEX #{table_name}_#{column_name}_index"
269
+ def remove_index(table_name, options) #:nodoc:
270
+ if Hash === options
271
+ index_name = options[:name]
272
+ else
273
+ index_name = "#{table_name}_#{options}_index"
274
+ end
275
+
276
+ execute "DROP INDEX #{index_name}"
168
277
  end
278
+
169
279
 
170
280
  private
171
281
  BYTEA_COLUMN_TYPE_OID = 17
172
282
 
173
- def last_insert_id(table, column = "id")
174
- sequence_name = "#{table}_#{column || 'id'}_seq"
175
- @connection.exec("SELECT currval('#{sequence_name}')")[0][0].to_i
283
+ def last_insert_id(table, sequence_name)
284
+ if sequence_name
285
+ @connection.exec("SELECT currval('#{sequence_name}')")[0][0].to_i
286
+ end
176
287
  end
177
288
 
178
289
  def select(sql, name = nil)
@@ -196,18 +307,54 @@ module ActiveRecord
196
307
  return rows
197
308
  end
198
309
 
199
- def quote_bytea(s)
200
- "'#{escape_bytea(s)}'"
201
- end
202
-
203
310
  def escape_bytea(s)
204
- s.gsub(/\\/) { '\\\\\\\\' }.gsub(/[^\\]/) { |c| sprintf('\\\\%03o', c[0].to_i) } unless s.nil?
311
+ if PGconn.respond_to? :escape_bytea
312
+ self.class.send(:define_method, :escape_bytea) do |s|
313
+ PGconn.escape_bytea(s) if s
314
+ end
315
+ else
316
+ self.class.send(:define_method, :escape_bytea) do |s|
317
+ if s
318
+ result = ''
319
+ s.each_byte { |c| result << sprintf('\\\\%03o', c) }
320
+ result
321
+ end
322
+ end
323
+ end
324
+ escape_bytea(s)
205
325
  end
206
326
 
207
327
  def unescape_bytea(s)
208
- s.gsub(/\\([0-9][0-9][0-9])/) { $1.oct.chr }.gsub(/\\\\/) { '\\' } unless s.nil?
328
+ if PGconn.respond_to? :unescape_bytea
329
+ self.class.send(:define_method, :unescape_bytea) do |s|
330
+ PGconn.unescape_bytea(s) if s
331
+ end
332
+ else
333
+ self.class.send(:define_method, :unescape_bytea) do |s|
334
+ if s
335
+ result = ''
336
+ i, max = 0, s.size
337
+ while i < max
338
+ char = s[i]
339
+ if char == ?\\
340
+ if s[i+1] == ?\\
341
+ char = ?\\
342
+ i += 1
343
+ else
344
+ char = s[i+1..i+3].oct
345
+ i += 3
346
+ end
347
+ end
348
+ result << char
349
+ i += 1
350
+ end
351
+ result
352
+ end
353
+ end
354
+ end
355
+ unescape_bytea(s)
209
356
  end
210
-
357
+
211
358
  # Query a table's column names, default values, and types.
212
359
  #
213
360
  # The underlying query is roughly:
@@ -228,7 +375,7 @@ module ActiveRecord
228
375
  # - ::regclass is a function that gives the id for a table name
229
376
  def column_definitions(table_name)
230
377
  query <<-end_sql
231
- SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc
378
+ SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull
232
379
  FROM pg_attribute a LEFT JOIN pg_attrdef d
233
380
  ON a.attrelid = d.adrelid AND a.attnum = d.adnum
234
381
  WHERE a.attrelid = '#{table_name}'::regclass
@@ -246,6 +393,8 @@ module ActiveRecord
246
393
  when /^timestamp/i then 'datetime'
247
394
  when /^real|^money/i then 'float'
248
395
  when /^interval/i then 'string'
396
+ # geometric types (the line type is currently not implemented in postgresql)
397
+ when /^(?:point|lseg|box|"?path"?|polygon|circle)/i then 'string'
249
398
  when /^bytea/i then 'binary'
250
399
  else field_type # Pass through standard types.
251
400
  end
@@ -263,7 +412,7 @@ module ActiveRecord
263
412
  return value if value =~ /^[0-9]+(\.[0-9]*)?/
264
413
 
265
414
  # Date / Time magic values
266
- return Time.now.to_s if value =~ /^\('now'::text\)::(date|timestamp)/
415
+ return Time.now.to_s if value =~ /^now\(\)|^\('now'::text\)::(date|timestamp)/i
267
416
 
268
417
  # Fixed dates / times
269
418
  return $1 if value =~ /^'(.+)'::(date|timestamp)/
@@ -1,6 +1,5 @@
1
- # sqlite_adapter.rb
2
- # author: Luke Holden <lholden@cablelan.net>
3
- # updated for SQLite3: Jamis Buck <jamis_buck@byu.edu>
1
+ # Author: Luke Holden <lholden@cablelan.net>
2
+ # Updated for SQLite3: Jamis Buck <jamis@37signals.com>
4
3
 
5
4
  require 'active_record/connection_adapters/abstract_adapter'
6
5
 
@@ -51,8 +50,10 @@ module ActiveRecord
51
50
  raise ArgumentError, "No database file specified. Missing argument: dbfile"
52
51
  end
53
52
 
54
- # Allow database path relative to RAILS_ROOT.
55
- if Object.const_defined?(:RAILS_ROOT)
53
+ # Allow database path relative to RAILS_ROOT, but only if
54
+ # the database path is not the special path that tells
55
+ # Sqlite build a database only in memory.
56
+ if Object.const_defined?(:RAILS_ROOT) && ':memory:' != config[:dbfile]
56
57
  config[:dbfile] = File.expand_path(config[:dbfile], RAILS_ROOT)
57
58
  end
58
59
  end
@@ -61,22 +62,24 @@ module ActiveRecord
61
62
 
62
63
  module ConnectionAdapters #:nodoc:
63
64
  class SQLiteColumn < Column #:nodoc:
64
- def string_to_binary(value)
65
- value.gsub(/(\0|\%)/) do
66
- case $1
67
- when "\0" then "%00"
68
- when "%" then "%25"
69
- end
70
- end
71
- end
72
-
73
- def binary_to_string(value)
74
- value.gsub(/(%00|%25)/) do
75
- case $1
76
- when "%00" then "\0"
77
- when "%25" then "%"
78
- end
79
- end
65
+ class << self
66
+ def string_to_binary(value)
67
+ value.gsub(/\0|\%/) do |b|
68
+ case b
69
+ when "\0" then "%00"
70
+ when "%" then "%25"
71
+ end
72
+ end
73
+ end
74
+
75
+ def binary_to_string(value)
76
+ value.gsub(/%00|%25/) do |b|
77
+ case b
78
+ when "%00" then "\0"
79
+ when "%25" then "%"
80
+ end
81
+ end
82
+ end
80
83
  end
81
84
  end
82
85
 
@@ -87,7 +90,15 @@ module ActiveRecord
87
90
  #
88
91
  # * <tt>:dbfile</tt> -- Path to the database file.
89
92
  class SQLiteAdapter < AbstractAdapter
90
- def native_database_types
93
+ def adapter_name #:nodoc:
94
+ 'SQLite'
95
+ end
96
+
97
+ def supports_migrations? #:nodoc:
98
+ true
99
+ end
100
+
101
+ def native_database_types #:nodoc:
91
102
  {
92
103
  :primary_key => "INTEGER PRIMARY KEY NOT NULL",
93
104
  :string => { :name => "varchar", :limit => 255 },
@@ -99,32 +110,45 @@ module ActiveRecord
99
110
  :time => { :name => "datetime" },
100
111
  :date => { :name => "date" },
101
112
  :binary => { :name => "blob" },
102
- :boolean => { :name => "integer" }
113
+ :boolean => { :name => "boolean" }
103
114
  }
104
115
  end
105
116
 
106
- def execute(sql, name = nil)
107
- #log(sql, name, @connection) { |connection| connection.execute(sql) }
117
+
118
+ # QUOTING ==================================================
119
+
120
+ def quote_string(s) #:nodoc:
121
+ @connection.class.quote(s)
122
+ end
123
+
124
+ def quote_column_name(name) #:nodoc:
125
+ "'#{name}'"
126
+ end
127
+
128
+
129
+ # DATABASE STATEMENTS ======================================
130
+
131
+ def execute(sql, name = nil) #:nodoc:
108
132
  log(sql, name) { @connection.execute(sql) }
109
133
  end
110
134
 
111
- def update(sql, name = nil)
135
+ def update(sql, name = nil) #:nodoc:
112
136
  execute(sql, name)
113
137
  @connection.changes
114
138
  end
115
139
 
116
- def delete(sql, name = nil)
140
+ def delete(sql, name = nil) #:nodoc:
117
141
  sql += " WHERE 1=1" unless sql =~ /WHERE/i
118
142
  execute(sql, name)
119
143
  @connection.changes
120
144
  end
121
145
 
122
- def insert(sql, name = nil, pk = nil, id_value = nil)
146
+ def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
123
147
  execute(sql, name = nil)
124
148
  id_value || @connection.last_insert_row_id
125
149
  end
126
150
 
127
- def select_all(sql, name = nil)
151
+ def select_all(sql, name = nil) #:nodoc:
128
152
  execute(sql, name).map do |row|
129
153
  record = {}
130
154
  row.each_key do |key|
@@ -136,43 +160,168 @@ module ActiveRecord
136
160
  end
137
161
  end
138
162
 
139
- def select_one(sql, name = nil)
163
+ def select_one(sql, name = nil) #:nodoc:
140
164
  result = select_all(sql, name)
141
165
  result.nil? ? nil : result.first
142
166
  end
143
167
 
144
168
 
145
- def begin_db_transaction() @connection.transaction end
146
- def commit_db_transaction() @connection.commit end
147
- def rollback_db_transaction() @connection.rollback end
169
+ def begin_db_transaction #:nodoc:
170
+ @connection.transaction
171
+ end
172
+
173
+ def commit_db_transaction #:nodoc:
174
+ @connection.commit
175
+ end
176
+
177
+ def rollback_db_transaction #:nodoc:
178
+ @connection.rollback
179
+ end
180
+
148
181
 
182
+ # SCHEMA STATEMENTS ========================================
149
183
 
150
- def tables
151
- execute('.table').map { |table| Table.new(table) }
184
+ def tables(name = nil) #:nodoc:
185
+ execute("SELECT name FROM sqlite_master WHERE type = 'table'", name).map do |row|
186
+ row[0]
187
+ end
152
188
  end
153
189
 
154
- def columns(table_name, name = nil)
190
+ def columns(table_name, name = nil) #:nodoc:
155
191
  table_structure(table_name).map { |field|
156
- SQLiteColumn.new(field['name'], field['dflt_value'], field['type'])
192
+ SQLiteColumn.new(field['name'], field['dflt_value'], field['type'], field['notnull'] == "0")
157
193
  }
158
194
  end
159
195
 
160
- def quote_string(s)
161
- @connection.class.quote(s)
196
+ def indexes(table_name, name = nil) #:nodoc:
197
+ execute("PRAGMA index_list(#{table_name})", name).map do |row|
198
+ index = IndexDefinition.new(table_name, row['name'])
199
+ index.unique = row['unique'] != '0'
200
+ index.columns = execute("PRAGMA index_info('#{index.name}')").map { |col| col['name'] }
201
+ index
202
+ end
162
203
  end
163
204
 
164
- def quote_column_name(name)
165
- "'#{name}'"
205
+ def primary_key(table_name) #:nodoc:
206
+ column = table_structure(table_name).find {|field| field['pk'].to_i == 1}
207
+ column ? column['name'] : nil
166
208
  end
167
209
 
168
- def adapter_name()
169
- 'SQLite'
210
+ def remove_index(table_name, options={}) #:nodoc:
211
+ if Hash === options
212
+ index_name = options[:name]
213
+ else
214
+ index_name = "#{table_name}_#{options}_index"
215
+ end
216
+
217
+ execute "DROP INDEX #{index_name}"
218
+ end
219
+
220
+ def rename_table(name, new_name)
221
+ move_table(name, new_name)
170
222
  end
171
223
 
224
+ def add_column(table_name, column_name, type, options = {}) #:nodoc:
225
+ alter_table(table_name) do |definition|
226
+ definition.column(column_name, type, options)
227
+ end
228
+ end
229
+
230
+ def remove_column(table_name, column_name) #:nodoc:
231
+ alter_table(table_name) do |definition|
232
+ definition.columns.delete(definition[column_name])
233
+ end
234
+ end
235
+
236
+ def change_column_default(table_name, column_name, default) #:nodoc:
237
+ alter_table(table_name) do |definition|
238
+ definition[column_name].default = default
239
+ end
240
+ end
241
+
242
+ def change_column(table_name, column_name, type, options = {}) #:nodoc:
243
+ alter_table(table_name) do |definition|
244
+ definition[column_name].instance_eval do
245
+ self.type = type
246
+ self.limit = options[:limit] if options[:limit]
247
+ self.default = options[:default] if options[:default]
248
+ end
249
+ end
250
+ end
251
+
252
+ def rename_column(table_name, column_name, new_column_name) #:nodoc:
253
+ alter_table(table_name, :rename => {column_name => new_column_name})
254
+ end
255
+
172
256
 
173
257
  protected
174
258
  def table_structure(table_name)
175
- execute "PRAGMA table_info(#{table_name})"
259
+ returning structure = execute("PRAGMA table_info(#{table_name})") do
260
+ raise ActiveRecord::StatementInvalid if structure.empty?
261
+ end
262
+ end
263
+
264
+ def alter_table(table_name, options = {}) #:nodoc:
265
+ altered_table_name = "altered_#{table_name}"
266
+ caller = lambda {|definition| yield definition if block_given?}
267
+
268
+ transaction do
269
+ move_table(table_name, altered_table_name,
270
+ options.merge(:temporary => true))
271
+ move_table(altered_table_name, table_name, &caller)
272
+ end
273
+ end
274
+
275
+ def move_table(from, to, options = {}, &block) #:nodoc:
276
+ copy_table(from, to, options, &block)
277
+ drop_table(from)
278
+ end
279
+
280
+ def copy_table(from, to, options = {}) #:nodoc:
281
+ create_table(to, options) do |@definition|
282
+ columns(from).each do |column|
283
+ column_name = options[:rename][column.name] if
284
+ options[:rename][column.name] if options[:rename]
285
+
286
+ @definition.column(column_name || column.name, column.type,
287
+ :limit => column.limit, :default => column.default,
288
+ :null => column.null)
289
+ end
290
+ @definition.primary_key(primary_key(from))
291
+ yield @definition if block_given?
292
+ end
293
+
294
+ copy_table_indexes(from, to)
295
+ copy_table_contents(from, to,
296
+ @definition.columns.map {|column| column.name},
297
+ options[:rename] || {})
298
+ end
299
+
300
+ def copy_table_indexes(from, to) #:nodoc:
301
+ indexes(from).each do |index|
302
+ name = index.name
303
+ if to == "altered_#{from}"
304
+ name = "temp_#{name}"
305
+ elsif from == "altered_#{to}"
306
+ name = name[5..-1]
307
+ end
308
+
309
+ opts = { :name => name }
310
+ opts[:unique] = true if index.unique
311
+ add_index(to, index.columns, opts)
312
+ end
313
+ end
314
+
315
+ def copy_table_contents(from, to, columns, rename = {}) #:nodoc:
316
+ column_mappings = Hash[*columns.map {|name| [name, name]}.flatten]
317
+ rename.inject(column_mappings) {|map, a| map[a.last] = a.first; map}
318
+
319
+ @connection.execute "SELECT * FROM #{from}" do |row|
320
+ sql = "INSERT INTO #{to} VALUES ("
321
+ sql << columns.map {|col| quote row[column_mappings[col]]} * ', '
322
+ sql << ')'
323
+ @connection.execute sql
324
+ end
176
325
  end
177
326
  end
178
327