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.
- data/CHANGELOG +198 -0
- data/lib/active_record.rb +19 -14
- data/lib/active_record/acts/list.rb +8 -6
- data/lib/active_record/acts/tree.rb +33 -10
- data/lib/active_record/aggregations.rb +1 -7
- data/lib/active_record/associations.rb +151 -82
- data/lib/active_record/associations/association_collection.rb +25 -0
- data/lib/active_record/associations/association_proxy.rb +9 -8
- data/lib/active_record/associations/belongs_to_association.rb +19 -5
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +44 -69
- data/lib/active_record/associations/has_many_association.rb +6 -14
- data/lib/active_record/associations/has_one_association.rb +5 -3
- data/lib/active_record/base.rb +344 -130
- data/lib/active_record/callbacks.rb +2 -2
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +128 -0
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +104 -0
- data/lib/active_record/connection_adapters/abstract/quoting.rb +51 -0
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +249 -0
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +245 -0
- data/lib/active_record/connection_adapters/abstract_adapter.rb +29 -464
- data/lib/active_record/connection_adapters/db2_adapter.rb +40 -10
- data/lib/active_record/connection_adapters/mysql_adapter.rb +131 -60
- data/lib/active_record/connection_adapters/oci_adapter.rb +106 -26
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +211 -62
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +193 -44
- data/lib/active_record/connection_adapters/sqlserver_adapter.rb +24 -15
- data/lib/active_record/fixtures.rb +47 -24
- data/lib/active_record/migration.rb +34 -5
- data/lib/active_record/observer.rb +32 -2
- data/lib/active_record/query_cache.rb +12 -11
- data/lib/active_record/schema.rb +58 -0
- data/lib/active_record/schema_dumper.rb +84 -0
- data/lib/active_record/transactions.rb +1 -3
- data/lib/active_record/validations.rb +40 -26
- data/lib/active_record/vendor/mysql.rb +6 -0
- data/lib/active_record/version.rb +9 -0
- data/rakefile +5 -16
- data/test/abstract_unit.rb +6 -11
- data/test/adapter_test.rb +58 -0
- data/test/ar_schema_test.rb +33 -0
- data/test/association_callbacks_test.rb +14 -0
- data/test/associations_go_eager_test.rb +56 -14
- data/test/associations_test.rb +245 -25
- data/test/base_test.rb +205 -34
- data/test/binary_test.rb +25 -42
- data/test/callbacks_test.rb +75 -0
- data/test/conditions_scoping_test.rb +136 -0
- data/test/connections/native_mysql/connection.rb +0 -4
- data/test/connections/native_sqlite3/in_memory_connection.rb +17 -0
- data/test/copy_table_sqlite.rb +64 -0
- data/test/deprecated_associations_test.rb +7 -6
- data/test/deprecated_finder_test.rb +3 -3
- data/test/finder_test.rb +33 -3
- data/test/fixtures/accounts.yml +5 -0
- data/test/fixtures/categories_ordered.yml +7 -0
- data/test/fixtures/category.rb +11 -1
- data/test/fixtures/comment.rb +22 -2
- data/test/fixtures/comments.yml +6 -0
- data/test/fixtures/companies.yml +15 -0
- data/test/fixtures/company.rb +24 -1
- data/test/fixtures/db_definitions/db2.drop.sql +5 -1
- data/test/fixtures/db_definitions/db2.sql +15 -1
- data/test/fixtures/db_definitions/mysql.drop.sql +2 -0
- data/test/fixtures/db_definitions/mysql.sql +17 -2
- data/test/fixtures/db_definitions/oci.drop.sql +37 -5
- data/test/fixtures/db_definitions/oci.sql +47 -4
- data/test/fixtures/db_definitions/oci2.drop.sql +1 -1
- data/test/fixtures/db_definitions/oci2.sql +2 -2
- data/test/fixtures/db_definitions/postgresql.drop.sql +4 -0
- data/test/fixtures/db_definitions/postgresql.sql +33 -4
- data/test/fixtures/db_definitions/sqlite.drop.sql +2 -0
- data/test/fixtures/db_definitions/sqlite.sql +16 -2
- data/test/fixtures/db_definitions/sqlserver.drop.sql +2 -0
- data/test/fixtures/db_definitions/sqlserver.sql +16 -2
- data/test/fixtures/developer.rb +1 -1
- data/test/fixtures/flowers.jpg +0 -0
- data/test/fixtures/keyboard.rb +3 -0
- data/test/fixtures/mixins.yml +11 -1
- data/test/fixtures/order.rb +4 -0
- data/test/fixtures/post.rb +4 -0
- data/test/fixtures/posts.yml +7 -0
- data/test/fixtures/project.rb +1 -0
- data/test/fixtures/subject.rb +4 -0
- data/test/fixtures/subscriber.rb +2 -4
- data/test/fixtures/topics.yml +2 -2
- data/test/fixtures_test.rb +79 -7
- data/test/inheritance_test.rb +2 -2
- data/test/lifecycle_test.rb +14 -6
- data/test/migration_test.rb +164 -6
- data/test/mixin_test.rb +78 -2
- data/test/pk_test.rb +25 -1
- data/test/readonly_test.rb +31 -0
- data/test/reflection_test.rb +4 -1
- data/test/schema_dumper_test.rb +19 -0
- data/test/schema_test_postgresql.rb +3 -2
- data/test/synonym_test_oci.rb +17 -0
- data/test/threaded_connections_test.rb +2 -1
- data/test/transactions_test.rb +109 -10
- data/test/validations_test.rb +70 -42
- metadata +25 -5
- data/test/fixtures/associations.png +0 -0
- 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
|
-
|
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
|
-
|
84
|
-
|
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
|
88
|
-
|
89
|
-
result.nil? ? nil : result.first
|
86
|
+
def quote_column_name(name)
|
87
|
+
%("#{name}")
|
90
88
|
end
|
91
89
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
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
|
123
|
-
|
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
|
131
|
-
|
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
|
-
|
135
|
-
|
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
|
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,
|
167
|
-
|
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,
|
174
|
-
sequence_name
|
175
|
-
|
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
|
-
|
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
|
-
|
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 =~
|
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
|
-
#
|
2
|
-
#
|
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
|
-
|
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
|
-
|
65
|
-
value
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
value
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
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 => "
|
113
|
+
:boolean => { :name => "boolean" }
|
103
114
|
}
|
104
115
|
end
|
105
116
|
|
106
|
-
|
107
|
-
|
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
|
146
|
-
|
147
|
-
|
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('
|
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
|
161
|
-
|
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
|
165
|
-
|
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
|
169
|
-
|
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
|
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
|
|