activerecord 3.2.22.5 → 4.0.0.beta1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1024 -543
- data/MIT-LICENSE +1 -1
- data/README.rdoc +20 -29
- data/examples/performance.rb +1 -1
- data/lib/active_record.rb +55 -44
- data/lib/active_record/aggregations.rb +40 -34
- data/lib/active_record/associations.rb +204 -276
- data/lib/active_record/associations/alias_tracker.rb +1 -1
- data/lib/active_record/associations/association.rb +30 -35
- data/lib/active_record/associations/association_scope.rb +40 -40
- data/lib/active_record/associations/belongs_to_association.rb +15 -2
- data/lib/active_record/associations/builder/association.rb +81 -28
- data/lib/active_record/associations/builder/belongs_to.rb +35 -57
- data/lib/active_record/associations/builder/collection_association.rb +54 -40
- data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +23 -41
- data/lib/active_record/associations/builder/has_many.rb +8 -64
- data/lib/active_record/associations/builder/has_one.rb +13 -50
- data/lib/active_record/associations/builder/singular_association.rb +13 -13
- data/lib/active_record/associations/collection_association.rb +92 -88
- data/lib/active_record/associations/collection_proxy.rb +913 -63
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +12 -10
- data/lib/active_record/associations/has_many_association.rb +35 -9
- data/lib/active_record/associations/has_many_through_association.rb +24 -14
- data/lib/active_record/associations/has_one_association.rb +33 -13
- data/lib/active_record/associations/has_one_through_association.rb +1 -1
- data/lib/active_record/associations/join_dependency.rb +2 -2
- data/lib/active_record/associations/join_dependency/join_association.rb +17 -22
- data/lib/active_record/associations/join_dependency/join_part.rb +1 -1
- data/lib/active_record/associations/join_helper.rb +1 -11
- data/lib/active_record/associations/preloader.rb +14 -17
- data/lib/active_record/associations/preloader/association.rb +29 -33
- data/lib/active_record/associations/preloader/collection_association.rb +1 -1
- data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +1 -1
- data/lib/active_record/associations/preloader/has_many_through.rb +1 -1
- data/lib/active_record/associations/preloader/has_one.rb +1 -1
- data/lib/active_record/associations/preloader/through_association.rb +13 -17
- data/lib/active_record/associations/singular_association.rb +11 -11
- data/lib/active_record/associations/through_association.rb +2 -2
- data/lib/active_record/attribute_assignment.rb +133 -153
- data/lib/active_record/attribute_methods.rb +196 -93
- data/lib/active_record/attribute_methods/before_type_cast.rb +44 -5
- data/lib/active_record/attribute_methods/dirty.rb +31 -28
- data/lib/active_record/attribute_methods/primary_key.rb +38 -30
- data/lib/active_record/attribute_methods/query.rb +5 -4
- data/lib/active_record/attribute_methods/read.rb +62 -91
- data/lib/active_record/attribute_methods/serialization.rb +97 -66
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +39 -45
- data/lib/active_record/attribute_methods/write.rb +32 -39
- data/lib/active_record/autosave_association.rb +56 -70
- data/lib/active_record/base.rb +53 -450
- data/lib/active_record/callbacks.rb +53 -18
- data/lib/active_record/coders/yaml_column.rb +11 -9
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +353 -197
- data/lib/active_record/connection_adapters/abstract/database_limits.rb +9 -0
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +130 -131
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +24 -19
- data/lib/active_record/connection_adapters/abstract/quoting.rb +23 -3
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +101 -91
- data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +59 -0
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +225 -96
- data/lib/active_record/connection_adapters/abstract/transaction.rb +203 -0
- data/lib/active_record/connection_adapters/abstract_adapter.rb +99 -46
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +114 -36
- data/lib/active_record/connection_adapters/column.rb +46 -24
- data/lib/active_record/connection_adapters/connection_specification.rb +96 -0
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +16 -32
- data/lib/active_record/connection_adapters/mysql_adapter.rb +181 -64
- data/lib/active_record/connection_adapters/postgresql/array_parser.rb +97 -0
- data/lib/active_record/connection_adapters/postgresql/cast.rb +132 -0
- data/lib/active_record/connection_adapters/postgresql/database_statements.rb +242 -0
- data/lib/active_record/connection_adapters/postgresql/oid.rb +347 -0
- data/lib/active_record/connection_adapters/postgresql/quoting.rb +158 -0
- data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +30 -0
- data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +448 -0
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +454 -885
- data/lib/active_record/connection_adapters/schema_cache.rb +48 -16
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +574 -13
- data/lib/active_record/connection_handling.rb +98 -0
- data/lib/active_record/core.rb +428 -0
- data/lib/active_record/counter_cache.rb +106 -108
- data/lib/active_record/dynamic_matchers.rb +110 -63
- data/lib/active_record/errors.rb +25 -8
- data/lib/active_record/explain.rb +8 -58
- data/lib/active_record/explain_subscriber.rb +6 -3
- data/lib/active_record/fixture_set/file.rb +56 -0
- data/lib/active_record/fixtures.rb +146 -148
- data/lib/active_record/inheritance.rb +77 -59
- data/lib/active_record/integration.rb +5 -5
- data/lib/active_record/locale/en.yml +8 -1
- data/lib/active_record/locking/optimistic.rb +38 -42
- data/lib/active_record/locking/pessimistic.rb +4 -4
- data/lib/active_record/log_subscriber.rb +19 -9
- data/lib/active_record/migration.rb +318 -153
- data/lib/active_record/migration/command_recorder.rb +90 -31
- data/lib/active_record/migration/join_table.rb +15 -0
- data/lib/active_record/model_schema.rb +69 -92
- data/lib/active_record/nested_attributes.rb +113 -148
- data/lib/active_record/null_relation.rb +65 -0
- data/lib/active_record/persistence.rb +188 -97
- data/lib/active_record/query_cache.rb +18 -36
- data/lib/active_record/querying.rb +19 -15
- data/lib/active_record/railtie.rb +91 -36
- data/lib/active_record/railties/console_sandbox.rb +0 -2
- data/lib/active_record/railties/controller_runtime.rb +2 -2
- data/lib/active_record/railties/databases.rake +90 -309
- data/lib/active_record/railties/jdbcmysql_error.rb +1 -1
- data/lib/active_record/readonly_attributes.rb +7 -3
- data/lib/active_record/reflection.rb +72 -56
- data/lib/active_record/relation.rb +241 -157
- data/lib/active_record/relation/batches.rb +25 -22
- data/lib/active_record/relation/calculations.rb +143 -121
- data/lib/active_record/relation/delegation.rb +96 -18
- data/lib/active_record/relation/finder_methods.rb +117 -183
- data/lib/active_record/relation/merger.rb +133 -0
- data/lib/active_record/relation/predicate_builder.rb +90 -42
- data/lib/active_record/relation/query_methods.rb +666 -136
- data/lib/active_record/relation/spawn_methods.rb +43 -150
- data/lib/active_record/result.rb +33 -6
- data/lib/active_record/sanitization.rb +24 -50
- data/lib/active_record/schema.rb +19 -12
- data/lib/active_record/schema_dumper.rb +31 -39
- data/lib/active_record/schema_migration.rb +36 -0
- data/lib/active_record/scoping.rb +0 -124
- data/lib/active_record/scoping/default.rb +48 -45
- data/lib/active_record/scoping/named.rb +74 -103
- data/lib/active_record/serialization.rb +6 -2
- data/lib/active_record/serializers/xml_serializer.rb +9 -15
- data/lib/active_record/store.rb +119 -15
- data/lib/active_record/tasks/database_tasks.rb +158 -0
- data/lib/active_record/tasks/mysql_database_tasks.rb +138 -0
- data/lib/active_record/tasks/postgresql_database_tasks.rb +90 -0
- data/lib/active_record/tasks/sqlite_database_tasks.rb +51 -0
- data/lib/active_record/test_case.rb +61 -38
- data/lib/active_record/timestamp.rb +8 -9
- data/lib/active_record/transactions.rb +65 -51
- data/lib/active_record/validations.rb +17 -15
- data/lib/active_record/validations/associated.rb +20 -14
- data/lib/active_record/validations/presence.rb +65 -0
- data/lib/active_record/validations/uniqueness.rb +93 -52
- data/lib/active_record/version.rb +4 -4
- data/lib/rails/generators/active_record.rb +3 -5
- data/lib/rails/generators/active_record/migration/migration_generator.rb +37 -7
- data/lib/rails/generators/active_record/migration/templates/migration.rb +20 -15
- data/lib/rails/generators/active_record/model/model_generator.rb +4 -3
- data/lib/rails/generators/active_record/model/templates/model.rb +1 -6
- data/lib/rails/generators/active_record/model/templates/module.rb +1 -1
- metadata +53 -46
- data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +0 -32
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +0 -191
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +0 -583
- data/lib/active_record/dynamic_finder_match.rb +0 -68
- data/lib/active_record/dynamic_scope_match.rb +0 -23
- data/lib/active_record/fixtures/file.rb +0 -65
- data/lib/active_record/identity_map.rb +0 -162
- data/lib/active_record/observer.rb +0 -121
- data/lib/active_record/session_store.rb +0 -360
- data/lib/rails/generators/active_record/migration.rb +0 -15
- data/lib/rails/generators/active_record/observer/observer_generator.rb +0 -15
- data/lib/rails/generators/active_record/observer/templates/observer.rb +0 -4
- data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +0 -25
- data/lib/rails/generators/active_record/session_migration/templates/migration.rb +0 -12
@@ -1,26 +1,17 @@
|
|
1
1
|
module ActiveRecord
|
2
2
|
module ConnectionAdapters
|
3
3
|
class SchemaCache
|
4
|
-
attr_reader :primary_keys, :tables
|
5
|
-
|
4
|
+
attr_reader :primary_keys, :tables, :version
|
5
|
+
attr_accessor :connection
|
6
6
|
|
7
7
|
def initialize(conn)
|
8
8
|
@connection = conn
|
9
|
-
@tables = {}
|
10
9
|
|
11
|
-
@columns
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
h[table_name] = Hash[columns[table_name].map { |col|
|
17
|
-
[col.name, col]
|
18
|
-
}]
|
19
|
-
end
|
20
|
-
|
21
|
-
@primary_keys = Hash.new do |h, table_name|
|
22
|
-
h[table_name] = table_exists?(table_name) ? connection.primary_key(table_name) : nil
|
23
|
-
end
|
10
|
+
@columns = {}
|
11
|
+
@columns_hash = {}
|
12
|
+
@primary_keys = {}
|
13
|
+
@tables = {}
|
14
|
+
prepare_default_proc
|
24
15
|
end
|
25
16
|
|
26
17
|
# A cached lookup for table existence.
|
@@ -30,6 +21,15 @@ module ActiveRecord
|
|
30
21
|
@tables[name] = connection.table_exists?(name)
|
31
22
|
end
|
32
23
|
|
24
|
+
# Add internal cache for table with +table_name+.
|
25
|
+
def add(table_name)
|
26
|
+
if table_exists?(table_name)
|
27
|
+
@primary_keys[table_name]
|
28
|
+
@columns[table_name]
|
29
|
+
@columns_hash[table_name]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
33
|
# Get the columns for a table
|
34
34
|
def columns(table = nil)
|
35
35
|
if table
|
@@ -55,6 +55,7 @@ module ActiveRecord
|
|
55
55
|
@columns_hash.clear
|
56
56
|
@primary_keys.clear
|
57
57
|
@tables.clear
|
58
|
+
@version = nil
|
58
59
|
end
|
59
60
|
|
60
61
|
# Clear out internal caches for table with +table_name+.
|
@@ -64,6 +65,37 @@ module ActiveRecord
|
|
64
65
|
@primary_keys.delete table_name
|
65
66
|
@tables.delete table_name
|
66
67
|
end
|
68
|
+
|
69
|
+
def marshal_dump
|
70
|
+
# if we get current version during initialization, it happens stack over flow.
|
71
|
+
@version = ActiveRecord::Migrator.current_version
|
72
|
+
[@version] + [:@columns, :@columns_hash, :@primary_keys, :@tables].map do |val|
|
73
|
+
self.instance_variable_get(val).inject({}) { |h, v| h[v[0]] = v[1]; h }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def marshal_load(array)
|
78
|
+
@version, @columns, @columns_hash, @primary_keys, @tables = array
|
79
|
+
prepare_default_proc
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def prepare_default_proc
|
85
|
+
@columns.default_proc = Proc.new do |h, table_name|
|
86
|
+
h[table_name] = connection.columns(table_name)
|
87
|
+
end
|
88
|
+
|
89
|
+
@columns_hash.default_proc = Proc.new do |h, table_name|
|
90
|
+
h[table_name] = Hash[columns[table_name].map { |col|
|
91
|
+
[col.name, col]
|
92
|
+
}]
|
93
|
+
end
|
94
|
+
|
95
|
+
@primary_keys.default_proc = Proc.new do |h, table_name|
|
96
|
+
h[table_name] = table_exists?(table_name) ? connection.primary_key(table_name) : nil
|
97
|
+
end
|
98
|
+
end
|
67
99
|
end
|
68
100
|
end
|
69
101
|
end
|
@@ -1,12 +1,14 @@
|
|
1
|
-
require 'active_record/connection_adapters/
|
1
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
2
|
+
require 'active_record/connection_adapters/statement_pool'
|
3
|
+
require 'arel/visitors/bind_visitor'
|
2
4
|
|
3
|
-
gem 'sqlite3', '~> 1.3.
|
5
|
+
gem 'sqlite3', '~> 1.3.6'
|
4
6
|
require 'sqlite3'
|
5
7
|
|
6
8
|
module ActiveRecord
|
7
|
-
|
9
|
+
module ConnectionHandling
|
8
10
|
# sqlite3 adapter reuses sqlite_connection.
|
9
|
-
def
|
11
|
+
def sqlite3_connection(config) # :nodoc:
|
10
12
|
# Require database.
|
11
13
|
unless config[:database]
|
12
14
|
raise ArgumentError, "No database file specified. Missing argument: database"
|
@@ -19,23 +21,208 @@ module ActiveRecord
|
|
19
21
|
config[:database] = File.expand_path(config[:database], Rails.root)
|
20
22
|
end
|
21
23
|
|
22
|
-
unless 'sqlite3' == config[:adapter]
|
23
|
-
raise ArgumentError, 'adapter name should be "sqlite3"'
|
24
|
-
end
|
25
|
-
|
26
24
|
db = SQLite3::Database.new(
|
27
25
|
config[:database],
|
28
26
|
:results_as_hash => true
|
29
27
|
)
|
30
28
|
|
31
|
-
db.busy_timeout(config[:timeout]) if config[:timeout]
|
29
|
+
db.busy_timeout(ConnectionAdapters::SQLite3Adapter.type_cast_config_to_integer(config[:timeout])) if config[:timeout]
|
32
30
|
|
33
31
|
ConnectionAdapters::SQLite3Adapter.new(db, logger, config)
|
34
32
|
end
|
35
33
|
end
|
36
34
|
|
37
35
|
module ConnectionAdapters #:nodoc:
|
38
|
-
class
|
36
|
+
class SQLite3Column < Column #:nodoc:
|
37
|
+
class << self
|
38
|
+
def binary_to_string(value)
|
39
|
+
if value.encoding != Encoding::ASCII_8BIT
|
40
|
+
value = value.force_encoding(Encoding::ASCII_8BIT)
|
41
|
+
end
|
42
|
+
value
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# The SQLite3 adapter works SQLite 3.6.16 or newer
|
48
|
+
# with the sqlite3-ruby drivers (available as gem from https://rubygems.org/gems/sqlite3).
|
49
|
+
#
|
50
|
+
# Options:
|
51
|
+
#
|
52
|
+
# * <tt>:database</tt> - Path to the database file.
|
53
|
+
class SQLite3Adapter < AbstractAdapter
|
54
|
+
class Version
|
55
|
+
include Comparable
|
56
|
+
|
57
|
+
def initialize(version_string)
|
58
|
+
@version = version_string.split('.').map { |v| v.to_i }
|
59
|
+
end
|
60
|
+
|
61
|
+
def <=>(version_string)
|
62
|
+
@version <=> version_string.split('.').map { |v| v.to_i }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class StatementPool < ConnectionAdapters::StatementPool
|
67
|
+
def initialize(connection, max)
|
68
|
+
super
|
69
|
+
@cache = Hash.new { |h,pid| h[pid] = {} }
|
70
|
+
end
|
71
|
+
|
72
|
+
def each(&block); cache.each(&block); end
|
73
|
+
def key?(key); cache.key?(key); end
|
74
|
+
def [](key); cache[key]; end
|
75
|
+
def length; cache.length; end
|
76
|
+
|
77
|
+
def []=(sql, key)
|
78
|
+
while @max <= cache.size
|
79
|
+
dealloc(cache.shift.last[:stmt])
|
80
|
+
end
|
81
|
+
cache[sql] = key
|
82
|
+
end
|
83
|
+
|
84
|
+
def clear
|
85
|
+
cache.values.each do |hash|
|
86
|
+
dealloc hash[:stmt]
|
87
|
+
end
|
88
|
+
cache.clear
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
def cache
|
93
|
+
@cache[$$]
|
94
|
+
end
|
95
|
+
|
96
|
+
def dealloc(stmt)
|
97
|
+
stmt.close unless stmt.closed?
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class BindSubstitution < Arel::Visitors::SQLite # :nodoc:
|
102
|
+
include Arel::Visitors::BindVisitor
|
103
|
+
end
|
104
|
+
|
105
|
+
def initialize(connection, logger, config)
|
106
|
+
super(connection, logger)
|
107
|
+
|
108
|
+
@active = nil
|
109
|
+
@statements = StatementPool.new(@connection,
|
110
|
+
self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
|
111
|
+
@config = config
|
112
|
+
|
113
|
+
if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
|
114
|
+
@visitor = Arel::Visitors::SQLite.new self
|
115
|
+
else
|
116
|
+
@visitor = BindSubstitution.new self
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def adapter_name #:nodoc:
|
121
|
+
'SQLite'
|
122
|
+
end
|
123
|
+
|
124
|
+
# Returns true
|
125
|
+
def supports_ddl_transactions?
|
126
|
+
true
|
127
|
+
end
|
128
|
+
|
129
|
+
# Returns true if SQLite version is '3.6.8' or greater, false otherwise.
|
130
|
+
def supports_savepoints?
|
131
|
+
sqlite_version >= '3.6.8'
|
132
|
+
end
|
133
|
+
|
134
|
+
# Returns true, since this connection adapter supports prepared statement
|
135
|
+
# caching.
|
136
|
+
def supports_statement_cache?
|
137
|
+
true
|
138
|
+
end
|
139
|
+
|
140
|
+
# Returns true, since this connection adapter supports migrations.
|
141
|
+
def supports_migrations? #:nodoc:
|
142
|
+
true
|
143
|
+
end
|
144
|
+
|
145
|
+
# Returns true.
|
146
|
+
def supports_primary_key? #:nodoc:
|
147
|
+
true
|
148
|
+
end
|
149
|
+
|
150
|
+
def requires_reloading?
|
151
|
+
true
|
152
|
+
end
|
153
|
+
|
154
|
+
# Returns true
|
155
|
+
def supports_add_column?
|
156
|
+
true
|
157
|
+
end
|
158
|
+
|
159
|
+
def active?
|
160
|
+
@active != false
|
161
|
+
end
|
162
|
+
|
163
|
+
# Disconnects from the database if already connected. Otherwise, this
|
164
|
+
# method does nothing.
|
165
|
+
def disconnect!
|
166
|
+
super
|
167
|
+
@active = false
|
168
|
+
@connection.close rescue nil
|
169
|
+
end
|
170
|
+
|
171
|
+
# Clears the prepared statements cache.
|
172
|
+
def clear_cache!
|
173
|
+
@statements.clear
|
174
|
+
end
|
175
|
+
|
176
|
+
# Returns true
|
177
|
+
def supports_count_distinct? #:nodoc:
|
178
|
+
true
|
179
|
+
end
|
180
|
+
|
181
|
+
# Returns true
|
182
|
+
def supports_autoincrement? #:nodoc:
|
183
|
+
true
|
184
|
+
end
|
185
|
+
|
186
|
+
def supports_index_sort_order?
|
187
|
+
true
|
188
|
+
end
|
189
|
+
|
190
|
+
# Returns 62. SQLite supports index names up to 64
|
191
|
+
# characters. The rest is used by rails internally to perform
|
192
|
+
# temporary rename operations
|
193
|
+
def allowed_index_name_length
|
194
|
+
index_name_length - 2
|
195
|
+
end
|
196
|
+
|
197
|
+
def native_database_types #:nodoc:
|
198
|
+
{
|
199
|
+
:primary_key => default_primary_key_type,
|
200
|
+
:string => { :name => "varchar", :limit => 255 },
|
201
|
+
:text => { :name => "text" },
|
202
|
+
:integer => { :name => "integer" },
|
203
|
+
:float => { :name => "float" },
|
204
|
+
:decimal => { :name => "decimal" },
|
205
|
+
:datetime => { :name => "datetime" },
|
206
|
+
:timestamp => { :name => "datetime" },
|
207
|
+
:time => { :name => "time" },
|
208
|
+
:date => { :name => "date" },
|
209
|
+
:binary => { :name => "blob" },
|
210
|
+
:boolean => { :name => "boolean" }
|
211
|
+
}
|
212
|
+
end
|
213
|
+
|
214
|
+
# Returns the current database encoding format as a string, eg: 'UTF-8'
|
215
|
+
def encoding
|
216
|
+
@connection.encoding.to_s
|
217
|
+
end
|
218
|
+
|
219
|
+
# Returns true.
|
220
|
+
def supports_explain?
|
221
|
+
true
|
222
|
+
end
|
223
|
+
|
224
|
+
# QUOTING ==================================================
|
225
|
+
|
39
226
|
def quote(value, column = nil)
|
40
227
|
if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
|
41
228
|
s = column.class.string_to_binary(value).unpack("H*")[0]
|
@@ -45,11 +232,385 @@ module ActiveRecord
|
|
45
232
|
end
|
46
233
|
end
|
47
234
|
|
48
|
-
|
49
|
-
|
50
|
-
|
235
|
+
def quote_string(s) #:nodoc:
|
236
|
+
@connection.class.quote(s)
|
237
|
+
end
|
238
|
+
|
239
|
+
def quote_table_name_for_assignment(table, attr)
|
240
|
+
quote_column_name(attr)
|
241
|
+
end
|
242
|
+
|
243
|
+
def quote_column_name(name) #:nodoc:
|
244
|
+
%Q("#{name.to_s.gsub('"', '""')}")
|
245
|
+
end
|
246
|
+
|
247
|
+
# Quote date/time values for use in SQL input. Includes microseconds
|
248
|
+
# if the value is a Time responding to usec.
|
249
|
+
def quoted_date(value) #:nodoc:
|
250
|
+
if value.respond_to?(:usec)
|
251
|
+
"#{super}.#{sprintf("%06d", value.usec)}"
|
252
|
+
else
|
253
|
+
super
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
def type_cast(value, column) # :nodoc:
|
258
|
+
return value.to_f if BigDecimal === value
|
259
|
+
return super unless String === value
|
260
|
+
return super unless column && value
|
261
|
+
|
262
|
+
value = super
|
263
|
+
if column.type == :string && value.encoding == Encoding::ASCII_8BIT
|
264
|
+
logger.error "Binary data inserted for `string` type on column `#{column.name}`" if logger
|
265
|
+
value = value.encode Encoding::UTF_8
|
266
|
+
end
|
267
|
+
value
|
268
|
+
end
|
269
|
+
|
270
|
+
# DATABASE STATEMENTS ======================================
|
271
|
+
|
272
|
+
def explain(arel, binds = [])
|
273
|
+
sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}"
|
274
|
+
ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds))
|
275
|
+
end
|
276
|
+
|
277
|
+
class ExplainPrettyPrinter
|
278
|
+
# Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles
|
279
|
+
# the output of the SQLite shell:
|
280
|
+
#
|
281
|
+
# 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows)
|
282
|
+
# 0|1|1|SCAN TABLE posts (~100000 rows)
|
283
|
+
#
|
284
|
+
def pp(result) # :nodoc:
|
285
|
+
result.rows.map do |row|
|
286
|
+
row.join('|')
|
287
|
+
end.join("\n") + "\n"
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
def exec_query(sql, name = nil, binds = [])
|
292
|
+
log(sql, name, binds) do
|
293
|
+
|
294
|
+
# Don't cache statements without bind values
|
295
|
+
if binds.empty?
|
296
|
+
stmt = @connection.prepare(sql)
|
297
|
+
cols = stmt.columns
|
298
|
+
records = stmt.to_a
|
299
|
+
stmt.close
|
300
|
+
stmt = records
|
301
|
+
else
|
302
|
+
cache = @statements[sql] ||= {
|
303
|
+
:stmt => @connection.prepare(sql)
|
304
|
+
}
|
305
|
+
stmt = cache[:stmt]
|
306
|
+
cols = cache[:cols] ||= stmt.columns
|
307
|
+
stmt.reset!
|
308
|
+
stmt.bind_params binds.map { |col, val|
|
309
|
+
type_cast(val, col)
|
310
|
+
}
|
311
|
+
end
|
312
|
+
|
313
|
+
ActiveRecord::Result.new(cols, stmt.to_a)
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
def exec_delete(sql, name = 'SQL', binds = [])
|
318
|
+
exec_query(sql, name, binds)
|
319
|
+
@connection.changes
|
320
|
+
end
|
321
|
+
alias :exec_update :exec_delete
|
322
|
+
|
323
|
+
def last_inserted_id(result)
|
324
|
+
@connection.last_insert_row_id
|
325
|
+
end
|
326
|
+
|
327
|
+
def execute(sql, name = nil) #:nodoc:
|
328
|
+
log(sql, name) { @connection.execute(sql) }
|
329
|
+
end
|
330
|
+
|
331
|
+
def update_sql(sql, name = nil) #:nodoc:
|
332
|
+
super
|
333
|
+
@connection.changes
|
334
|
+
end
|
335
|
+
|
336
|
+
def delete_sql(sql, name = nil) #:nodoc:
|
337
|
+
sql += " WHERE 1=1" unless sql =~ /WHERE/i
|
338
|
+
super sql, name
|
339
|
+
end
|
340
|
+
|
341
|
+
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
|
342
|
+
super
|
343
|
+
id_value || @connection.last_insert_row_id
|
344
|
+
end
|
345
|
+
alias :create :insert_sql
|
346
|
+
|
347
|
+
def select_rows(sql, name = nil)
|
348
|
+
exec_query(sql, name).rows
|
349
|
+
end
|
350
|
+
|
351
|
+
def create_savepoint
|
352
|
+
execute("SAVEPOINT #{current_savepoint_name}")
|
353
|
+
end
|
354
|
+
|
355
|
+
def rollback_to_savepoint
|
356
|
+
execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
|
357
|
+
end
|
358
|
+
|
359
|
+
def release_savepoint
|
360
|
+
execute("RELEASE SAVEPOINT #{current_savepoint_name}")
|
361
|
+
end
|
362
|
+
|
363
|
+
def begin_db_transaction #:nodoc:
|
364
|
+
log('begin transaction',nil) { @connection.transaction }
|
365
|
+
end
|
366
|
+
|
367
|
+
def commit_db_transaction #:nodoc:
|
368
|
+
log('commit transaction',nil) { @connection.commit }
|
369
|
+
end
|
370
|
+
|
371
|
+
def rollback_db_transaction #:nodoc:
|
372
|
+
log('rollback transaction',nil) { @connection.rollback }
|
373
|
+
end
|
374
|
+
|
375
|
+
# SCHEMA STATEMENTS ========================================
|
376
|
+
|
377
|
+
def tables(name = nil, table_name = nil) #:nodoc:
|
378
|
+
sql = <<-SQL
|
379
|
+
SELECT name
|
380
|
+
FROM sqlite_master
|
381
|
+
WHERE type = 'table' AND NOT name = 'sqlite_sequence'
|
382
|
+
SQL
|
383
|
+
sql << " AND name = #{quote_table_name(table_name)}" if table_name
|
384
|
+
|
385
|
+
exec_query(sql, 'SCHEMA').map do |row|
|
386
|
+
row['name']
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
def table_exists?(table_name)
|
391
|
+
table_name && tables(nil, table_name).any?
|
392
|
+
end
|
393
|
+
|
394
|
+
# Returns an array of +SQLite3Column+ objects for the table specified by +table_name+.
|
395
|
+
def columns(table_name) #:nodoc:
|
396
|
+
table_structure(table_name).map do |field|
|
397
|
+
case field["dflt_value"]
|
398
|
+
when /^null$/i
|
399
|
+
field["dflt_value"] = nil
|
400
|
+
when /^'(.*)'$/m
|
401
|
+
field["dflt_value"] = $1.gsub("''", "'")
|
402
|
+
when /^"(.*)"$/m
|
403
|
+
field["dflt_value"] = $1.gsub('""', '"')
|
404
|
+
end
|
405
|
+
|
406
|
+
SQLite3Column.new(field['name'], field['dflt_value'], field['type'], field['notnull'].to_i == 0)
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
# Returns an array of indexes for the given table.
|
411
|
+
def indexes(table_name, name = nil) #:nodoc:
|
412
|
+
exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", 'SCHEMA').map do |row|
|
413
|
+
IndexDefinition.new(
|
414
|
+
table_name,
|
415
|
+
row['name'],
|
416
|
+
row['unique'] != 0,
|
417
|
+
exec_query("PRAGMA index_info('#{row['name']}')", "SCHEMA").map { |col|
|
418
|
+
col['name']
|
419
|
+
})
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
def primary_key(table_name) #:nodoc:
|
424
|
+
column = table_structure(table_name).find { |field|
|
425
|
+
field['pk'] == 1
|
426
|
+
}
|
427
|
+
column && column['name']
|
428
|
+
end
|
429
|
+
|
430
|
+
def remove_index!(table_name, index_name) #:nodoc:
|
431
|
+
exec_query "DROP INDEX #{quote_column_name(index_name)}"
|
51
432
|
end
|
52
433
|
|
434
|
+
# Renames a table.
|
435
|
+
#
|
436
|
+
# Example:
|
437
|
+
# rename_table('octopuses', 'octopi')
|
438
|
+
def rename_table(table_name, new_name)
|
439
|
+
exec_query "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
|
440
|
+
rename_table_indexes(table_name, new_name)
|
441
|
+
end
|
442
|
+
|
443
|
+
# See: http://www.sqlite.org/lang_altertable.html
|
444
|
+
# SQLite has an additional restriction on the ALTER TABLE statement
|
445
|
+
def valid_alter_table_options( type, options)
|
446
|
+
type.to_sym != :primary_key
|
447
|
+
end
|
448
|
+
|
449
|
+
def add_column(table_name, column_name, type, options = {}) #:nodoc:
|
450
|
+
if supports_add_column? && valid_alter_table_options( type, options )
|
451
|
+
super(table_name, column_name, type, options)
|
452
|
+
else
|
453
|
+
alter_table(table_name) do |definition|
|
454
|
+
definition.column(column_name, type, options)
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
def remove_column(table_name, column_name, type = nil, options = {}) #:nodoc:
|
460
|
+
alter_table(table_name) do |definition|
|
461
|
+
definition.columns.delete(definition[column_name])
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
465
|
+
def change_column_default(table_name, column_name, default) #:nodoc:
|
466
|
+
alter_table(table_name) do |definition|
|
467
|
+
definition[column_name].default = default
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
def change_column_null(table_name, column_name, null, default = nil)
|
472
|
+
unless null || default.nil?
|
473
|
+
exec_query("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
|
474
|
+
end
|
475
|
+
alter_table(table_name) do |definition|
|
476
|
+
definition[column_name].null = null
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
def change_column(table_name, column_name, type, options = {}) #:nodoc:
|
481
|
+
alter_table(table_name) do |definition|
|
482
|
+
include_default = options_include_default?(options)
|
483
|
+
definition[column_name].instance_eval do
|
484
|
+
self.type = type
|
485
|
+
self.limit = options[:limit] if options.include?(:limit)
|
486
|
+
self.default = options[:default] if include_default
|
487
|
+
self.null = options[:null] if options.include?(:null)
|
488
|
+
self.precision = options[:precision] if options.include?(:precision)
|
489
|
+
self.scale = options[:scale] if options.include?(:scale)
|
490
|
+
end
|
491
|
+
end
|
492
|
+
end
|
493
|
+
|
494
|
+
def rename_column(table_name, column_name, new_column_name) #:nodoc:
|
495
|
+
unless columns(table_name).detect{|c| c.name == column_name.to_s }
|
496
|
+
raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}"
|
497
|
+
end
|
498
|
+
alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s})
|
499
|
+
rename_column_indexes(table_name, column_name, new_column_name)
|
500
|
+
end
|
501
|
+
|
502
|
+
protected
|
503
|
+
def select(sql, name = nil, binds = []) #:nodoc:
|
504
|
+
exec_query(sql, name, binds)
|
505
|
+
end
|
506
|
+
|
507
|
+
def table_structure(table_name)
|
508
|
+
structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA').to_hash
|
509
|
+
raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
|
510
|
+
structure
|
511
|
+
end
|
512
|
+
|
513
|
+
def alter_table(table_name, options = {}) #:nodoc:
|
514
|
+
altered_table_name = "a#{table_name}"
|
515
|
+
caller = lambda {|definition| yield definition if block_given?}
|
516
|
+
|
517
|
+
transaction do
|
518
|
+
move_table(table_name, altered_table_name,
|
519
|
+
options.merge(:temporary => true))
|
520
|
+
move_table(altered_table_name, table_name, &caller)
|
521
|
+
end
|
522
|
+
end
|
523
|
+
|
524
|
+
def move_table(from, to, options = {}, &block) #:nodoc:
|
525
|
+
copy_table(from, to, options, &block)
|
526
|
+
drop_table(from)
|
527
|
+
end
|
528
|
+
|
529
|
+
def copy_table(from, to, options = {}) #:nodoc:
|
530
|
+
from_primary_key = primary_key(from)
|
531
|
+
options[:id] = false
|
532
|
+
create_table(to, options) do |definition|
|
533
|
+
@definition = definition
|
534
|
+
@definition.primary_key(from_primary_key) if from_primary_key.present?
|
535
|
+
columns(from).each do |column|
|
536
|
+
column_name = options[:rename] ?
|
537
|
+
(options[:rename][column.name] ||
|
538
|
+
options[:rename][column.name.to_sym] ||
|
539
|
+
column.name) : column.name
|
540
|
+
next if column_name == from_primary_key
|
541
|
+
|
542
|
+
@definition.column(column_name, column.type,
|
543
|
+
:limit => column.limit, :default => column.default,
|
544
|
+
:precision => column.precision, :scale => column.scale,
|
545
|
+
:null => column.null)
|
546
|
+
end
|
547
|
+
yield @definition if block_given?
|
548
|
+
end
|
549
|
+
copy_table_indexes(from, to, options[:rename] || {})
|
550
|
+
copy_table_contents(from, to,
|
551
|
+
@definition.columns.map {|column| column.name},
|
552
|
+
options[:rename] || {})
|
553
|
+
end
|
554
|
+
|
555
|
+
def copy_table_indexes(from, to, rename = {}) #:nodoc:
|
556
|
+
indexes(from).each do |index|
|
557
|
+
name = index.name
|
558
|
+
if to == "a#{from}"
|
559
|
+
name = "t#{name}"
|
560
|
+
elsif from == "a#{to}"
|
561
|
+
name = name[1..-1]
|
562
|
+
end
|
563
|
+
|
564
|
+
to_column_names = columns(to).map { |c| c.name }
|
565
|
+
columns = index.columns.map {|c| rename[c] || c }.select do |column|
|
566
|
+
to_column_names.include?(column)
|
567
|
+
end
|
568
|
+
|
569
|
+
unless columns.empty?
|
570
|
+
# index name can't be the same
|
571
|
+
opts = { name: name.gsub(/(^|_)(#{from})_/, "\\1#{to}_"), internal: true }
|
572
|
+
opts[:unique] = true if index.unique
|
573
|
+
add_index(to, columns, opts)
|
574
|
+
end
|
575
|
+
end
|
576
|
+
end
|
577
|
+
|
578
|
+
def copy_table_contents(from, to, columns, rename = {}) #:nodoc:
|
579
|
+
column_mappings = Hash[columns.map {|name| [name, name]}]
|
580
|
+
rename.each { |a| column_mappings[a.last] = a.first }
|
581
|
+
from_columns = columns(from).collect {|col| col.name}
|
582
|
+
columns = columns.find_all{|col| from_columns.include?(column_mappings[col])}
|
583
|
+
quoted_columns = columns.map { |col| quote_column_name(col) } * ','
|
584
|
+
|
585
|
+
quoted_to = quote_table_name(to)
|
586
|
+
exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row|
|
587
|
+
sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES ("
|
588
|
+
sql << columns.map {|col| quote row[column_mappings[col]]} * ', '
|
589
|
+
sql << ')'
|
590
|
+
exec_query sql
|
591
|
+
end
|
592
|
+
end
|
593
|
+
|
594
|
+
def sqlite_version
|
595
|
+
@sqlite_version ||= SQLite3Adapter::Version.new(select_value('select sqlite_version(*)'))
|
596
|
+
end
|
597
|
+
|
598
|
+
def default_primary_key_type
|
599
|
+
if supports_autoincrement?
|
600
|
+
'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL'
|
601
|
+
else
|
602
|
+
'INTEGER PRIMARY KEY NOT NULL'
|
603
|
+
end
|
604
|
+
end
|
605
|
+
|
606
|
+
def translate_exception(exception, message)
|
607
|
+
case exception.message
|
608
|
+
when /column(s)? .* (is|are) not unique/
|
609
|
+
RecordNotUnique.new(message, exception)
|
610
|
+
else
|
611
|
+
super
|
612
|
+
end
|
613
|
+
end
|
53
614
|
end
|
54
615
|
end
|
55
616
|
end
|