sequel 3.27.0 → 3.28.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +96 -0
- data/README.rdoc +2 -2
- data/Rakefile +1 -1
- data/doc/association_basics.rdoc +48 -0
- data/doc/opening_databases.rdoc +29 -5
- data/doc/prepared_statements.rdoc +1 -0
- data/doc/release_notes/3.28.0.txt +304 -0
- data/doc/testing.rdoc +42 -0
- data/doc/transactions.rdoc +97 -0
- data/lib/sequel/adapters/db2.rb +95 -65
- data/lib/sequel/adapters/firebird.rb +25 -219
- data/lib/sequel/adapters/ibmdb.rb +440 -0
- data/lib/sequel/adapters/jdbc.rb +12 -0
- data/lib/sequel/adapters/jdbc/as400.rb +0 -7
- data/lib/sequel/adapters/jdbc/db2.rb +49 -0
- data/lib/sequel/adapters/jdbc/firebird.rb +34 -0
- data/lib/sequel/adapters/jdbc/oracle.rb +2 -27
- data/lib/sequel/adapters/jdbc/transactions.rb +34 -0
- data/lib/sequel/adapters/mysql.rb +10 -15
- data/lib/sequel/adapters/odbc.rb +1 -2
- data/lib/sequel/adapters/odbc/db2.rb +5 -5
- data/lib/sequel/adapters/postgres.rb +71 -11
- data/lib/sequel/adapters/shared/db2.rb +290 -0
- data/lib/sequel/adapters/shared/firebird.rb +214 -0
- data/lib/sequel/adapters/shared/mssql.rb +18 -75
- data/lib/sequel/adapters/shared/mysql.rb +13 -0
- data/lib/sequel/adapters/shared/postgres.rb +52 -36
- data/lib/sequel/adapters/shared/sqlite.rb +32 -36
- data/lib/sequel/adapters/sqlite.rb +4 -8
- data/lib/sequel/adapters/tinytds.rb +7 -3
- data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +55 -0
- data/lib/sequel/core.rb +1 -1
- data/lib/sequel/database/connecting.rb +1 -1
- data/lib/sequel/database/misc.rb +6 -5
- data/lib/sequel/database/query.rb +1 -1
- data/lib/sequel/database/schema_generator.rb +2 -1
- data/lib/sequel/dataset/actions.rb +149 -33
- data/lib/sequel/dataset/features.rb +44 -7
- data/lib/sequel/dataset/misc.rb +9 -1
- data/lib/sequel/dataset/prepared_statements.rb +2 -2
- data/lib/sequel/dataset/query.rb +63 -10
- data/lib/sequel/dataset/sql.rb +22 -5
- data/lib/sequel/model.rb +3 -3
- data/lib/sequel/model/associations.rb +250 -27
- data/lib/sequel/model/base.rb +10 -16
- data/lib/sequel/plugins/many_through_many.rb +34 -2
- data/lib/sequel/plugins/prepared_statements_with_pk.rb +1 -1
- data/lib/sequel/sql.rb +94 -51
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/db2_spec.rb +146 -0
- data/spec/adapters/postgres_spec.rb +74 -6
- data/spec/adapters/spec_helper.rb +1 -0
- data/spec/adapters/sqlite_spec.rb +11 -0
- data/spec/core/database_spec.rb +7 -0
- data/spec/core/dataset_spec.rb +180 -17
- data/spec/core/expression_filters_spec.rb +107 -41
- data/spec/core/spec_helper.rb +11 -0
- data/spec/extensions/many_through_many_spec.rb +115 -1
- data/spec/extensions/prepared_statements_with_pk_spec.rb +3 -3
- data/spec/integration/associations_test.rb +193 -15
- data/spec/integration/database_test.rb +4 -2
- data/spec/integration/dataset_test.rb +215 -19
- data/spec/integration/plugin_test.rb +8 -5
- data/spec/integration/prepared_statement_test.rb +91 -98
- data/spec/integration/schema_test.rb +27 -11
- data/spec/integration/spec_helper.rb +10 -0
- data/spec/integration/type_test.rb +2 -2
- data/spec/model/association_reflection_spec.rb +91 -0
- data/spec/model/associations_spec.rb +13 -0
- data/spec/model/base_spec.rb +8 -21
- data/spec/model/eager_loading_spec.rb +243 -9
- data/spec/model/model_spec.rb +15 -2
- metadata +16 -4
data/doc/testing.rdoc
CHANGED
@@ -62,3 +62,45 @@ The order in which you delete/truncate the tables is important if you are using
|
|
62
62
|
[:table1, :table2].each{|x| Sequel::Model.db.from(x).delete}
|
63
63
|
end
|
64
64
|
end
|
65
|
+
|
66
|
+
= Testing Sequel Itself
|
67
|
+
|
68
|
+
Sequel has multiple separate test suites. All test suites run under either RSpec 1 or RSpec 2.
|
69
|
+
|
70
|
+
== rake spec
|
71
|
+
|
72
|
+
The +spec+ rake task (which is also the default rake task) runs Sequel's core and model specs. These specs use a mocked database connection, and test for specific SQL used and for generally correct behavior.
|
73
|
+
|
74
|
+
== rake spec_plugin
|
75
|
+
|
76
|
+
The +spec_plugin+ rake task runs the specs for the plugins and extensions that ship with Sequel. These also use a mocked database connection, and operate very similarly to the general Sequel core and model specs.
|
77
|
+
|
78
|
+
== rake spec_<i>adapter</i> (e.g. rake spec_postgres)
|
79
|
+
|
80
|
+
The <tt>spec_<i>adapter</i></tt> specs run against a real database connection with nothing mocked, and test for correct results. They are much slower than the standard specs, but they will catch errors that are mocked out by the default specs, as well show issues that only occur on a certain database, adapter, interpreter, or some combination of those.
|
81
|
+
|
82
|
+
These specs are broken down into two parts. For each database, there are specific specs that only apply to that database, and these are called the adapter specs. There are also shared specs that apply to all (or almost all) databases, these are called the integration specs.
|
83
|
+
|
84
|
+
== Environment variables
|
85
|
+
|
86
|
+
Sequel often uses environment variables when testing to specify either the database to be tested or specify how testing should be done.
|
87
|
+
|
88
|
+
=== Connection Strings
|
89
|
+
|
90
|
+
The following environment variables specify Database connection URL strings:
|
91
|
+
|
92
|
+
* SEQUEL_INTEGRATION_URL: integration specs
|
93
|
+
* SEQUEL_FB_SPEC_DB: firebird adapter specs
|
94
|
+
* SEQUEL_MSSQL_SPEC_DB: mssql adapter specs
|
95
|
+
* SEQUEL_PG_SPEC_DB: postgres adapter specs
|
96
|
+
* SEQUEL_SQLITE_SPEC_DB: sqlite adapter specs
|
97
|
+
* SEQUEL_MY_SPEC_DB: mysql adapter specs
|
98
|
+
* SEQUEL_DB2_SPEC_DB: db2 adapter specs
|
99
|
+
|
100
|
+
=== Other
|
101
|
+
|
102
|
+
* SEQUEL_MSSQL_SPEC_REQUIRE: Separate file to require when running mssql adapter specs
|
103
|
+
* SEQUEL_DB2_SPEC_REQUIRE: Separate file to require when running db2 adapter specs
|
104
|
+
* SEQUEL_COLUMNS_INTROSPECTION: Whehter to run the specs with the columns_introspection extension loaded by default
|
105
|
+
* SEQUEL_NO_PENDING: Don't mark any specs as pending, try running all specs
|
106
|
+
* SKIPPED_TEST_WARN: Warn when skipping any tests because libraries aren't available
|
@@ -0,0 +1,97 @@
|
|
1
|
+
= Database Transactions
|
2
|
+
|
3
|
+
Sequel uses autocommit mode by default for all of its database adapters, so in general in Sequel if you want to use database transactions, you need to be explicit about it. There are a few cases where transactions are used implicitly by default:
|
4
|
+
|
5
|
+
* Dataset#import to insert many records at once
|
6
|
+
* Model#save
|
7
|
+
* Model#destroy
|
8
|
+
* Migrations
|
9
|
+
* A few model plugins
|
10
|
+
|
11
|
+
Everywhere else, it is up to use to use a database transaction if you want to.
|
12
|
+
|
13
|
+
== Basic Transaction Usage
|
14
|
+
|
15
|
+
In Sequel, the <tt>Database#transaction</tt> method should be called if you want to use a database transaction. This method must be called with a block. If the block does not raise an exception, the transaction is committed:
|
16
|
+
|
17
|
+
DB.transaction do # BEGIN
|
18
|
+
DB[:foo].insert(1) # INSERT
|
19
|
+
end # COMMIT
|
20
|
+
|
21
|
+
If the block raises a Sequel::Rollback exception, the transaction is rolled back, but no exception is raised outside the block:
|
22
|
+
|
23
|
+
DB.transaction do # BEGIN
|
24
|
+
raise Sequel::Rollback
|
25
|
+
end # ROLLBACK
|
26
|
+
# no exception raised
|
27
|
+
|
28
|
+
If any other exception is raised, the transaction is rolled back, and the exception is raised outside the block:
|
29
|
+
|
30
|
+
# ArgumentError raised
|
31
|
+
DB.transaction do # BEGIN
|
32
|
+
raise ArgumentError
|
33
|
+
end # ROLLBACK
|
34
|
+
# ArgumentError raised
|
35
|
+
|
36
|
+
== Nested Transaction Calls / Savepoints
|
37
|
+
|
38
|
+
You can nest calls to transaction, which by default just reuses the existing transaction:
|
39
|
+
|
40
|
+
DB.transaction do # BEGIN
|
41
|
+
DB.transaction do
|
42
|
+
DB[:foo].insert(1) # INSERT
|
43
|
+
end
|
44
|
+
end # COMMIT
|
45
|
+
|
46
|
+
You can use the <tt>:savepoint => true</tt> option in the inner transaction to explicitly use a savepoint (if the database supports it):
|
47
|
+
|
48
|
+
DB.transaction do # BEGIN
|
49
|
+
DB.transaction(:savepoint => true) do # SAVEPOINT
|
50
|
+
DB[:foo].insert(1) # INSERT
|
51
|
+
end # RELEASE SAVEPOINT
|
52
|
+
end # COMMIT
|
53
|
+
|
54
|
+
If a Sequel::Rollback exception is raised inside the savepoint block, it will only rollback to the savepoint:
|
55
|
+
|
56
|
+
DB.transaction do # BEGIN
|
57
|
+
DB.transaction(:savepoint => true) do # SAVEPOINT
|
58
|
+
raise Sequel::Rollback
|
59
|
+
end # ROLLBACK TO SAVEPOINT
|
60
|
+
# no exception raised
|
61
|
+
end # COMMIT
|
62
|
+
|
63
|
+
Other exceptions, unless rescued inside the outer transaction block, will rollback the savepoint and the outer transactions, since they are reraised by the transaction code:
|
64
|
+
|
65
|
+
DB.transaction do # BEGIN
|
66
|
+
DB.transaction(:savepoint => true) do # SAVEPOINT
|
67
|
+
raise ArgumentError
|
68
|
+
end # ROLLBACK TO SAVEPOINT
|
69
|
+
end # ROLLBACK
|
70
|
+
# ArgumentError raised
|
71
|
+
|
72
|
+
== Prepared Transactions / Two-Phase Commit
|
73
|
+
|
74
|
+
Sequel supports database prepared transactions on PostreSQL, MySQL, and H2. With prepared transactions, at the end of the transaction, the transaction is not immediately committed (it acts like a rollback). Later, you can call +commit_prepared_transaction+ to commit the transaction or +rollback_prepared_transaction+ to roll the transaction back. Prepared transactions are usually used with distributed databases to make sure all databases commit the same transaction or none of them do.
|
75
|
+
|
76
|
+
To use prepared transactions in Sequel, you provide a string as the value of the :prepare option:
|
77
|
+
|
78
|
+
DB.transaction(:prepare => 'foo') do # BEGIN
|
79
|
+
DB[:foo].insert(1) # INSERT
|
80
|
+
end # PREPARE TRANSACTION 'foo'
|
81
|
+
|
82
|
+
Later, you can commit the prepared transaction:
|
83
|
+
|
84
|
+
DB.commit_prepared_transaction('foo')
|
85
|
+
|
86
|
+
or roll the prepared transaction back:
|
87
|
+
|
88
|
+
DB.rollback_prepared_transaction('foo')
|
89
|
+
|
90
|
+
== Transaction Isolation Levels
|
91
|
+
|
92
|
+
The SQL standard supports 4 isolation levels: READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, and SERIALIZABLE. Not all databases implement the levels as specified in the standard (or implement the levels at all), but on PostgreSQL, MySQL, and Microsoft SQL Server, you can specify which transaction isolation level you want to use via the :isolation option to <tt>Database#transaction</tt>. The isolation level is specified as one of the following symbols: :uncommitted, :committed, :repeatable, and :serializable. Using this option make Sequel use the correct transaction isolation syntax for your database:
|
93
|
+
|
94
|
+
DB.transaction(:isolation => :serializable) do # BEGIN
|
95
|
+
# SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
|
96
|
+
DB[:foo].insert(1) # INSERT
|
97
|
+
end # COMMIT
|
data/lib/sequel/adapters/db2.rb
CHANGED
@@ -1,23 +1,24 @@
|
|
1
1
|
require 'db2/db2cli'
|
2
|
+
Sequel.require %w'shared/db2', 'adapters'
|
2
3
|
|
3
4
|
module Sequel
|
4
5
|
module DB2
|
5
6
|
class Database < Sequel::Database
|
6
|
-
|
7
|
+
include DatabaseMethods
|
7
8
|
|
8
|
-
|
9
|
+
set_adapter_scheme :db2
|
9
10
|
|
10
11
|
TEMPORARY = 'GLOBAL TEMPORARY '.freeze
|
11
12
|
|
12
|
-
rc, @@env = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE)
|
13
|
+
rc, @@env = DB2CLI.SQLAllocHandle(DB2CLI::SQL_HANDLE_ENV, DB2CLI::SQL_NULL_HANDLE)
|
13
14
|
#check_error(rc, "Could not allocate DB2 environment")
|
14
15
|
|
15
16
|
def connect(server)
|
16
17
|
opts = server_opts(server)
|
17
|
-
rc, dbc = SQLAllocHandle(SQL_HANDLE_DBC, @@env)
|
18
|
+
rc, dbc = DB2CLI.SQLAllocHandle(DB2CLI::SQL_HANDLE_DBC, @@env)
|
18
19
|
check_error(rc, "Could not allocate database connection")
|
19
20
|
|
20
|
-
rc = SQLConnect(dbc, opts[:database], opts[:user], opts[:password])
|
21
|
+
rc = DB2CLI.SQLConnect(dbc, opts[:database], opts[:user], opts[:password])
|
21
22
|
check_error(rc, "Could not connect to database")
|
22
23
|
|
23
24
|
dbc
|
@@ -32,109 +33,138 @@ module Sequel
|
|
32
33
|
DB2::Dataset.new(self, opts)
|
33
34
|
end
|
34
35
|
|
35
|
-
def execute(sql, opts={})
|
36
|
+
def execute(sql, opts={}, &block)
|
37
|
+
synchronize(opts[:server]){|conn| log_connection_execute(conn, sql, &block)}
|
38
|
+
end
|
39
|
+
alias do execute
|
40
|
+
|
41
|
+
def execute_insert(sql, opts={})
|
36
42
|
synchronize(opts[:server]) do |conn|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
rc
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
check_error(rc, "Could not free statement")
|
43
|
+
log_connection_execute(conn, sql)
|
44
|
+
sql = "SELECT IDENTITY_VAL_LOCAL() FROM SYSIBM.SYSDUMMY1"
|
45
|
+
log_connection_execute(conn, sql) do |sth|
|
46
|
+
rc, name, buflen, datatype, size, digits, nullable = DB2CLI.SQLDescribeCol(sth, 1, 256)
|
47
|
+
check_error(rc, "Could not describe column")
|
48
|
+
if (rc = DB2CLI.SQLFetch(sth)) != DB2CLI::SQL_NO_DATA_FOUND
|
49
|
+
rc, v = DB2CLI.SQLGetData(sth, 1, datatype, size)
|
50
|
+
check_error(rc, "Could not get data")
|
51
|
+
if v.is_a?(String)
|
52
|
+
return v.to_i
|
53
|
+
else
|
54
|
+
return nil
|
55
|
+
end
|
56
|
+
end
|
52
57
|
end
|
53
58
|
end
|
54
59
|
end
|
55
|
-
alias_method :do, :execute
|
56
|
-
|
57
|
-
private
|
58
60
|
|
59
61
|
def check_error(rc, msg)
|
60
62
|
case rc
|
61
|
-
when SQL_SUCCESS, SQL_SUCCESS_WITH_INFO
|
63
|
+
when DB2CLI::SQL_SUCCESS, DB2CLI::SQL_SUCCESS_WITH_INFO
|
62
64
|
nil
|
63
65
|
else
|
64
66
|
raise DatabaseError, msg
|
65
67
|
end
|
66
68
|
end
|
67
69
|
|
70
|
+
private
|
71
|
+
|
72
|
+
def begin_transaction(conn, opts={})
|
73
|
+
log_yield(TRANSACTION_BEGIN){DB2CLI.SQLSetConnectAttr(conn, DB2CLI::SQL_ATTR_AUTOCOMMIT, DB2CLI::SQL_AUTOCOMMIT_OFF)}
|
74
|
+
conn
|
75
|
+
end
|
76
|
+
|
77
|
+
def rollback_transaction(conn, opts={})
|
78
|
+
log_yield(TRANSACTION_ROLLBACK){DB2CLI.SQLEndTran(DB2CLI::SQL_HANDLE_DBC, conn, DB2CLI::SQL_ROLLBACK)}
|
79
|
+
ensure
|
80
|
+
DB2CLI.SQLSetConnectAttr(conn, DB2CLI::SQL_ATTR_AUTOCOMMIT, DB2CLI::SQL_AUTOCOMMIT_ON)
|
81
|
+
end
|
82
|
+
|
83
|
+
def commit_transaction(conn, opts={})
|
84
|
+
log_yield(TRANSACTION_COMMIT){DB2CLI.SQLEndTran(DB2CLI::SQL_HANDLE_DBC, conn, DB2CLI::SQL_COMMIT)}
|
85
|
+
ensure
|
86
|
+
DB2CLI.SQLSetConnectAttr(conn, DB2CLI::SQL_ATTR_AUTOCOMMIT, DB2CLI::SQL_AUTOCOMMIT_ON)
|
87
|
+
end
|
88
|
+
|
89
|
+
def log_connection_execute(conn, sql)
|
90
|
+
rc, sth = DB2CLI.SQLAllocHandle(DB2CLI::SQL_HANDLE_STMT, conn)
|
91
|
+
check_error(rc, "Could not allocate statement")
|
92
|
+
|
93
|
+
begin
|
94
|
+
rc = log_yield(sql){DB2CLI.SQLExecDirect(sth, sql)}
|
95
|
+
check_error(rc, "Could not execute statement: #{sql}")
|
96
|
+
|
97
|
+
yield(sth) if block_given?
|
98
|
+
|
99
|
+
rc, rpc = DB2CLI.SQLRowCount(sth)
|
100
|
+
check_error(rc, "Could not get RPC")
|
101
|
+
rpc
|
102
|
+
ensure
|
103
|
+
rc = DB2CLI.SQLFreeHandle(DB2CLI::SQL_HANDLE_STMT, sth)
|
104
|
+
check_error(rc, "Could not free statement")
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
68
108
|
def disconnect_connection(conn)
|
69
|
-
rc = SQLDisconnect(conn)
|
109
|
+
rc = DB2CLI.SQLDisconnect(conn)
|
70
110
|
check_error(rc, "Could not disconnect from database")
|
71
111
|
|
72
|
-
rc = SQLFreeHandle(SQL_HANDLE_DBC, conn)
|
112
|
+
rc = DB2CLI.SQLFreeHandle(DB2CLI::SQL_HANDLE_DBC, conn)
|
73
113
|
check_error(rc, "Could not free Database handle")
|
74
114
|
end
|
75
115
|
end
|
76
116
|
|
77
117
|
class Dataset < Sequel::Dataset
|
118
|
+
include DatasetMethods
|
119
|
+
|
78
120
|
MAX_COL_SIZE = 256
|
79
121
|
|
80
122
|
def fetch_rows(sql)
|
81
123
|
execute(sql) do |sth|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
124
|
+
offset = @opts[:offset]
|
125
|
+
db = @db
|
126
|
+
i = 1
|
127
|
+
column_info = get_column_info(sth)
|
128
|
+
cols = column_info.map{|c| c.at(1)}
|
129
|
+
cols.delete(row_number_column) if offset
|
130
|
+
@columns = cols
|
131
|
+
while (rc = DB2CLI.SQLFetch(sth)) != DB2CLI::SQL_NO_DATA_FOUND
|
132
|
+
db.check_error(rc, "Could not fetch row")
|
133
|
+
row = {}
|
134
|
+
column_info.each do |i, c, t, s|
|
135
|
+
rc, v = DB2CLI.SQLGetData(sth, i, t, s)
|
136
|
+
db.check_error(rc, "Could not get data")
|
137
|
+
row[c] = convert_type(v)
|
138
|
+
end
|
139
|
+
row.delete(row_number_column) if offset
|
140
|
+
yield row
|
87
141
|
end
|
88
142
|
end
|
89
143
|
self
|
90
144
|
end
|
91
145
|
|
92
|
-
# DB2 supports window functions
|
93
|
-
def supports_window_functions?
|
94
|
-
true
|
95
|
-
end
|
96
|
-
|
97
146
|
private
|
98
147
|
|
99
|
-
# Modify the sql to limit the number of rows returned
|
100
|
-
def select_limit_sql(sql)
|
101
|
-
if l = @opts[:limit]
|
102
|
-
sql << " FETCH FIRST #{l == 1 ? 'ROW' : "#{literal(l)} ROWS"} ONLY"
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
148
|
def get_column_info(sth)
|
107
|
-
|
108
|
-
|
149
|
+
db = @db
|
150
|
+
rc, column_count = DB2CLI.SQLNumResultCols(sth)
|
151
|
+
db.check_error(rc, "Could not get number of result columns")
|
109
152
|
|
110
153
|
(1..column_count).map do |i|
|
111
|
-
rc, name, buflen, datatype, size, digits, nullable = SQLDescribeCol(sth, i, MAX_COL_SIZE)
|
112
|
-
|
113
|
-
|
114
|
-
{:name => name, :db2_type => datatype, :precision => size}
|
154
|
+
rc, name, buflen, datatype, size, digits, nullable = DB2CLI.SQLDescribeCol(sth, i, MAX_COL_SIZE)
|
155
|
+
db.check_error(rc, "Could not describe column")
|
156
|
+
[i, output_identifier(name), datatype, size]
|
115
157
|
end
|
116
158
|
end
|
117
159
|
|
118
|
-
def hash_row(sth)
|
119
|
-
row = {}
|
120
|
-
@column_info.each_with_index do |c, i|
|
121
|
-
rc, v = SQLGetData(sth, i+1, c[:db2_type], c[:precision])
|
122
|
-
@db.check_error(rc, "Could not get data")
|
123
|
-
|
124
|
-
row[output_identifier(c[:name])] = convert_type(v)
|
125
|
-
end
|
126
|
-
row
|
127
|
-
end
|
128
|
-
|
129
160
|
def convert_type(v)
|
130
161
|
case v
|
131
162
|
when DB2CLI::Date
|
132
|
-
|
163
|
+
Date.new(v.year, v.month, v.day)
|
133
164
|
when DB2CLI::Time
|
134
|
-
|
165
|
+
Sequel::SQLTime.create(v.hour, v.minute, v.second)
|
135
166
|
when DB2CLI::Timestamp
|
136
|
-
|
137
|
-
v.hour, v.minute, v.second, v.fraction)
|
167
|
+
Sequel.database_to_application_timestamp(v.to_s)
|
138
168
|
when DB2CLI::Null
|
139
169
|
nil
|
140
170
|
else
|
@@ -1,109 +1,64 @@
|
|
1
1
|
require 'fb'
|
2
|
+
Sequel.require 'adapters/shared/firebird'
|
2
3
|
|
3
4
|
module Sequel
|
4
5
|
# The Sequel Firebird adapter requires the ruby fb driver located at
|
5
6
|
# http://github.com/wishdev/fb.
|
6
7
|
module Firebird
|
7
8
|
class Database < Sequel::Database
|
9
|
+
include Sequel::Firebird::DatabaseMethods
|
10
|
+
|
8
11
|
set_adapter_scheme :firebird
|
9
12
|
|
10
|
-
|
11
|
-
TEMPORARY = 'GLOBAL TEMPORARY '.freeze
|
13
|
+
DISCONNECT_ERRORS = /Unsuccessful execution caused by a system error that precludes successful execution of subsequent statements/
|
12
14
|
|
13
|
-
# Add the primary_keys
|
15
|
+
# Add the primary_keys instance variables.
|
14
16
|
# so we can get the correct return values for inserted rows.
|
15
17
|
def initialize(*args)
|
16
18
|
super
|
17
19
|
@primary_keys = {}
|
18
|
-
@primary_key_sequences = {}
|
19
20
|
end
|
20
21
|
|
21
22
|
def connect(server)
|
22
23
|
opts = server_opts(server)
|
23
24
|
|
24
|
-
|
25
|
+
Fb::Database.new(
|
25
26
|
:database => "#{opts[:host]}:#{opts[:database]}",
|
26
27
|
:username => opts[:user],
|
27
|
-
:password => opts[:password])
|
28
|
-
conn = db.connect
|
29
|
-
conn.downcase_names = true
|
30
|
-
conn
|
31
|
-
end
|
32
|
-
|
33
|
-
def create_trigger(*args)
|
34
|
-
self << create_trigger_sql(*args)
|
28
|
+
:password => opts[:password]).connect
|
35
29
|
end
|
36
30
|
|
37
31
|
def dataset(opts = nil)
|
38
32
|
Firebird::Dataset.new(self, opts)
|
39
33
|
end
|
40
34
|
|
41
|
-
def drop_sequence(name)
|
42
|
-
self << drop_sequence_sql(name)
|
43
|
-
end
|
44
|
-
|
45
35
|
def execute(sql, opts={})
|
46
36
|
begin
|
47
37
|
synchronize(opts[:server]) do |conn|
|
38
|
+
if conn.transaction_started && !@transactions.include?(Thread.current)
|
39
|
+
conn.rollback
|
40
|
+
raise DatabaseDisconnectError, "transaction accidently left open, rolling back and disconnecting"
|
41
|
+
end
|
48
42
|
r = log_yield(sql){conn.execute(sql)}
|
49
43
|
yield(r) if block_given?
|
50
44
|
r
|
51
45
|
end
|
52
|
-
rescue => e
|
53
|
-
raise_error(e, :
|
46
|
+
rescue Fb::Error => e
|
47
|
+
raise_error(e, :disconnect=>DISCONNECT_ERRORS.match(e.message))
|
54
48
|
end
|
55
49
|
end
|
56
50
|
|
57
|
-
# Return primary key for the given table.
|
58
|
-
def primary_key(table, server=nil)
|
59
|
-
synchronize(server){|conn| primary_key_for_table(conn, table)}
|
60
|
-
end
|
61
|
-
|
62
|
-
# Returns primary key for the given table. This information is
|
63
|
-
# cached, and if the primary key for a table is changed, the
|
64
|
-
# @primary_keys instance variable should be reset manually.
|
65
|
-
def primary_key_for_table(conn, table)
|
66
|
-
@primary_keys[quote_identifier(table)] ||= conn.table_primary_key(quote_identifier(table))
|
67
|
-
end
|
68
|
-
|
69
|
-
def restart_sequence(*args)
|
70
|
-
self << restart_sequence_sql(*args)
|
71
|
-
end
|
72
|
-
|
73
|
-
def sequences(opts={})
|
74
|
-
ds = self[:"rdb$generators"].server(opts[:server]).filter(:"rdb$system_flag" => 0).select(:"rdb$generator_name")
|
75
|
-
block_given? ? yield(ds) : ds.map{|r| ds.send(:output_identifier, r[:"rdb$generator_name"])}
|
76
|
-
end
|
77
|
-
|
78
|
-
def tables(opts={})
|
79
|
-
ds = self[:"rdb$relations"].server(opts[:server]).filter(:"rdb$view_blr" => nil, Sequel::SQL::Function.new(:COALESCE, :"rdb$system_flag", 0) => 0).select(:"rdb$relation_name")
|
80
|
-
block_given? ? yield(ds) : ds.map{|r| ds.send(:output_identifier, r[:"rdb$relation_name"])}
|
81
|
-
end
|
82
|
-
|
83
51
|
private
|
84
52
|
|
85
|
-
# Use Firebird specific syntax for add column
|
86
|
-
def alter_table_sql(table, op)
|
87
|
-
case op[:op]
|
88
|
-
when :add_column
|
89
|
-
"ALTER TABLE #{quote_schema_table(table)} ADD #{column_definition_sql(op)}"
|
90
|
-
when :drop_column
|
91
|
-
"ALTER TABLE #{quote_schema_table(table)} DROP #{column_definition_sql(op)}"
|
92
|
-
when :rename_column
|
93
|
-
"ALTER TABLE #{quote_schema_table(table)} ALTER #{quote_identifier(op[:name])} TO #{quote_identifier(op[:new_name])}"
|
94
|
-
when :set_column_type
|
95
|
-
"ALTER TABLE #{quote_schema_table(table)} ALTER #{quote_identifier(op[:name])} TYPE #{type_literal(op)}"
|
96
|
-
else
|
97
|
-
super(table, op)
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
def auto_increment_sql()
|
102
|
-
AUTO_INCREMENT
|
103
|
-
end
|
104
|
-
|
105
53
|
def begin_transaction(conn, opts={})
|
106
|
-
log_yield(TRANSACTION_BEGIN)
|
54
|
+
log_yield(TRANSACTION_BEGIN) do
|
55
|
+
begin
|
56
|
+
conn.transaction
|
57
|
+
rescue Fb::Error => e
|
58
|
+
conn.rollback
|
59
|
+
raise_error(e, :disconnect=>true)
|
60
|
+
end
|
61
|
+
end
|
107
62
|
conn
|
108
63
|
end
|
109
64
|
|
@@ -111,60 +66,6 @@ module Sequel
|
|
111
66
|
log_yield(TRANSACTION_COMMIT){conn.commit}
|
112
67
|
end
|
113
68
|
|
114
|
-
def create_sequence_sql(name, opts={})
|
115
|
-
"CREATE SEQUENCE #{quote_identifier(name)}"
|
116
|
-
end
|
117
|
-
|
118
|
-
# Firebird gets an override because of the mess of creating a
|
119
|
-
# sequence and trigger for auto-incrementing primary keys.
|
120
|
-
def create_table_from_generator(name, generator, options)
|
121
|
-
drop_statement, create_statements = create_table_sql_list(name, generator, options)
|
122
|
-
(execute_ddl(drop_statement) rescue nil) if drop_statement
|
123
|
-
create_statements.each{|sql| execute_ddl(sql)}
|
124
|
-
end
|
125
|
-
|
126
|
-
def create_table_sql_list(name, generator, options={})
|
127
|
-
statements = [create_table_sql(name, generator, options)]
|
128
|
-
drop_seq_statement = nil
|
129
|
-
generator.columns.each do |c|
|
130
|
-
if c[:auto_increment]
|
131
|
-
c[:sequence_name] ||= "seq_#{name}_#{c[:name]}"
|
132
|
-
unless c[:create_sequence] == false
|
133
|
-
drop_seq_statement = drop_sequence_sql(c[:sequence_name])
|
134
|
-
statements << create_sequence_sql(c[:sequence_name])
|
135
|
-
statements << restart_sequence_sql(c[:sequence_name], {:restart_position => c[:sequence_start_position]}) if c[:sequence_start_position]
|
136
|
-
end
|
137
|
-
unless c[:create_trigger] == false
|
138
|
-
c[:trigger_name] ||= "BI_#{name}_#{c[:name]}"
|
139
|
-
c[:quoted_name] = quote_identifier(c[:name])
|
140
|
-
trigger_definition = <<-END
|
141
|
-
begin
|
142
|
-
if ((new.#{c[:quoted_name]} is null) or (new.#{c[:quoted_name]} = 0)) then
|
143
|
-
begin
|
144
|
-
new.#{c[:quoted_name]} = next value for #{c[:sequence_name]};
|
145
|
-
end
|
146
|
-
end
|
147
|
-
END
|
148
|
-
statements << create_trigger_sql(name, c[:trigger_name], trigger_definition, {:events => [:insert]})
|
149
|
-
end
|
150
|
-
end
|
151
|
-
end
|
152
|
-
[drop_seq_statement, statements]
|
153
|
-
end
|
154
|
-
|
155
|
-
def create_trigger_sql(table, name, definition, opts={})
|
156
|
-
events = opts[:events] ? Array(opts[:events]) : [:insert, :update, :delete]
|
157
|
-
whence = opts[:after] ? 'AFTER' : 'BEFORE'
|
158
|
-
inactive = opts[:inactive] ? 'INACTIVE' : 'ACTIVE'
|
159
|
-
position = opts.fetch(:position, 0)
|
160
|
-
sql = <<-end_sql
|
161
|
-
CREATE TRIGGER #{quote_identifier(name)} for #{quote_identifier(table)}
|
162
|
-
#{inactive} #{whence} #{events.map{|e| e.to_s.upcase}.join(' OR ')} position #{position}
|
163
|
-
as #{definition}
|
164
|
-
end_sql
|
165
|
-
sql
|
166
|
-
end
|
167
|
-
|
168
69
|
def database_error_classes
|
169
70
|
[Fb::Error]
|
170
71
|
end
|
@@ -173,42 +74,24 @@ module Sequel
|
|
173
74
|
c.close
|
174
75
|
end
|
175
76
|
|
176
|
-
def drop_sequence_sql(name)
|
177
|
-
"DROP SEQUENCE #{quote_identifier(name)}"
|
178
|
-
end
|
179
|
-
|
180
|
-
def restart_sequence_sql(name, opts={})
|
181
|
-
seq_name = quote_identifier(name)
|
182
|
-
"ALTER SEQUENCE #{seq_name} RESTART WITH #{opts[:restart_position]}"
|
183
|
-
end
|
184
|
-
|
185
77
|
def rollback_transaction(conn, opts={})
|
186
78
|
log_yield(TRANSACTION_ROLLBACK){conn.rollback}
|
187
79
|
end
|
188
|
-
|
189
|
-
def type_literal_generic_string(column)
|
190
|
-
column[:text] ? :"BLOB SUB_TYPE TEXT" : super
|
191
|
-
end
|
192
80
|
end
|
193
81
|
|
194
82
|
# Dataset class for Firebird datasets
|
195
83
|
class Dataset < Sequel::Dataset
|
196
|
-
|
197
|
-
BOOL_FALSE = '0'.freeze
|
198
|
-
NULL = LiteralString.new('NULL').freeze
|
199
|
-
COMMA_SEPARATOR = ', '.freeze
|
200
|
-
SELECT_CLAUSE_METHODS = clause_methods(:select, %w'with distinct limit columns from join where group having compounds order')
|
201
|
-
INSERT_CLAUSE_METHODS = clause_methods(:insert, %w'into columns values returning_select')
|
84
|
+
include Sequel::Firebird::DatasetMethods
|
202
85
|
|
203
86
|
# Yield all rows returned by executing the given SQL and converting
|
204
87
|
# the types.
|
205
88
|
def fetch_rows(sql)
|
206
89
|
execute(sql) do |s|
|
207
90
|
begin
|
208
|
-
@columns = s.fields.map{|c| output_identifier(c.name)}
|
209
|
-
s.fetchall
|
91
|
+
@columns = columns = s.fields.map{|c| output_identifier(c.name)}
|
92
|
+
s.fetchall.each do |r|
|
210
93
|
h = {}
|
211
|
-
r.each{|
|
94
|
+
r.zip(columns).each{|v, c| h[c] = v}
|
212
95
|
yield h
|
213
96
|
end
|
214
97
|
ensure
|
@@ -217,83 +100,6 @@ module Sequel
|
|
217
100
|
end
|
218
101
|
self
|
219
102
|
end
|
220
|
-
|
221
|
-
# Insert given values into the database.
|
222
|
-
def insert(*values)
|
223
|
-
if !@opts[:sql]
|
224
|
-
clone(default_server_opts(:sql=>insert_returning_pk_sql(*values))).single_value
|
225
|
-
else
|
226
|
-
execute_insert(insert_sql(*values), :table=>opts[:from].first,
|
227
|
-
:values=>values.size == 1 ? values.first : values)
|
228
|
-
end
|
229
|
-
end
|
230
|
-
|
231
|
-
# Use the RETURNING clause to return the primary key of the inserted record, if it exists
|
232
|
-
def insert_returning_pk_sql(*values)
|
233
|
-
pk = db.primary_key(opts[:from].first)
|
234
|
-
insert_returning_sql(pk ? Sequel::SQL::Identifier.new(pk) : NULL, *values)
|
235
|
-
end
|
236
|
-
|
237
|
-
# Use the RETURNING clause to return the columns listed in returning.
|
238
|
-
def insert_returning_sql(returning, *values)
|
239
|
-
"#{insert_sql(*values)} RETURNING #{column_list(Array(returning))}"
|
240
|
-
end
|
241
|
-
|
242
|
-
# Insert a record returning the record inserted
|
243
|
-
def insert_select(*values)
|
244
|
-
naked.clone(default_server_opts(:sql=>insert_returning_sql(nil, *values))).single_record
|
245
|
-
end
|
246
|
-
|
247
|
-
def requires_sql_standard_datetimes?
|
248
|
-
true
|
249
|
-
end
|
250
|
-
|
251
|
-
def supports_insert_select?
|
252
|
-
true
|
253
|
-
end
|
254
|
-
|
255
|
-
# Firebird does not support INTERSECT or EXCEPT
|
256
|
-
def supports_intersect_except?
|
257
|
-
false
|
258
|
-
end
|
259
|
-
|
260
|
-
private
|
261
|
-
|
262
|
-
def insert_clause_methods
|
263
|
-
INSERT_CLAUSE_METHODS
|
264
|
-
end
|
265
|
-
|
266
|
-
def insert_returning_select_sql(sql)
|
267
|
-
if opts.has_key?(:returning)
|
268
|
-
sql << " RETURNING #{column_list(Array(opts[:returning]))}"
|
269
|
-
end
|
270
|
-
end
|
271
|
-
|
272
|
-
def hash_row(stmt, row)
|
273
|
-
@columns.inject({}) do |m, c|
|
274
|
-
m[c] = row.shift
|
275
|
-
m
|
276
|
-
end
|
277
|
-
end
|
278
|
-
|
279
|
-
def literal_false
|
280
|
-
BOOL_FALSE
|
281
|
-
end
|
282
|
-
|
283
|
-
def literal_true
|
284
|
-
BOOL_TRUE
|
285
|
-
end
|
286
|
-
|
287
|
-
# The order of clauses in the SELECT SQL statement
|
288
|
-
def select_clause_methods
|
289
|
-
SELECT_CLAUSE_METHODS
|
290
|
-
end
|
291
|
-
|
292
|
-
def select_limit_sql(sql)
|
293
|
-
sql << " FIRST #{@opts[:limit]}" if @opts[:limit]
|
294
|
-
sql << " SKIP #{@opts[:offset]}" if @opts[:offset]
|
295
|
-
end
|
296
|
-
|
297
103
|
end
|
298
104
|
end
|
299
105
|
end
|