peasys-ruby 1.0.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/active_record/connection_adapters/peasys/column.rb +20 -0
- data/lib/active_record/connection_adapters/peasys/database_statements.rb +209 -0
- data/lib/active_record/connection_adapters/peasys/quoting.rb +99 -0
- data/lib/active_record/connection_adapters/peasys/schema_creation.rb +27 -0
- data/lib/active_record/connection_adapters/peasys/schema_definitions.rb +10 -0
- data/lib/active_record/connection_adapters/peasys/schema_dumper.rb +88 -0
- data/lib/active_record/connection_adapters/peasys/schema_statements.rb +431 -0
- data/lib/active_record/connection_adapters/peasys_adapter.rb +207 -0
- data/lib/arel/visitors/peasys.rb +53 -0
- data/lib/pea_client.rb +481 -477
- data/lib/pea_exception.rb +30 -30
- data/lib/pea_response.rb +299 -299
- data/lib/peasys-ruby.rb +11 -0
- metadata +67 -8
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module ConnectionAdapters
|
|
5
|
+
module Peasys
|
|
6
|
+
module SchemaStatements
|
|
7
|
+
def tables
|
|
8
|
+
schema = @config[:schema]&.upcase
|
|
9
|
+
sql = <<~SQL
|
|
10
|
+
SELECT TABLE_NAME
|
|
11
|
+
FROM QSYS2.SYSTABLES
|
|
12
|
+
WHERE TABLE_SCHEMA = '#{schema}'
|
|
13
|
+
AND TABLE_TYPE = 'T'
|
|
14
|
+
ORDER BY TABLE_NAME
|
|
15
|
+
SQL
|
|
16
|
+
query_values(sql, "SCHEMA").map { |name| name.strip.downcase }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def views
|
|
20
|
+
schema = @config[:schema]&.upcase
|
|
21
|
+
sql = <<~SQL
|
|
22
|
+
SELECT TABLE_NAME
|
|
23
|
+
FROM QSYS2.SYSTABLES
|
|
24
|
+
WHERE TABLE_SCHEMA = '#{schema}'
|
|
25
|
+
AND TABLE_TYPE = 'V'
|
|
26
|
+
ORDER BY TABLE_NAME
|
|
27
|
+
SQL
|
|
28
|
+
query_values(sql, "SCHEMA").map { |name| name.strip.downcase }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def table_exists?(table_name)
|
|
32
|
+
schema = @config[:schema]&.upcase
|
|
33
|
+
tbl = table_name.to_s.upcase
|
|
34
|
+
sql = <<~SQL
|
|
35
|
+
SELECT COUNT(*)
|
|
36
|
+
FROM QSYS2.SYSTABLES
|
|
37
|
+
WHERE TABLE_SCHEMA = '#{schema}'
|
|
38
|
+
AND TABLE_NAME = '#{tbl}'
|
|
39
|
+
AND TABLE_TYPE IN ('T', 'P')
|
|
40
|
+
SQL
|
|
41
|
+
query_value(sql, "SCHEMA").to_i > 0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def view_exists?(view_name)
|
|
45
|
+
schema = @config[:schema]&.upcase
|
|
46
|
+
vw = view_name.to_s.upcase
|
|
47
|
+
sql = <<~SQL
|
|
48
|
+
SELECT COUNT(*)
|
|
49
|
+
FROM QSYS2.SYSTABLES
|
|
50
|
+
WHERE TABLE_SCHEMA = '#{schema}'
|
|
51
|
+
AND TABLE_NAME = '#{vw}'
|
|
52
|
+
AND TABLE_TYPE = 'V'
|
|
53
|
+
SQL
|
|
54
|
+
query_value(sql, "SCHEMA").to_i > 0
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def primary_keys(table_name)
|
|
58
|
+
schema = @config[:schema]&.upcase
|
|
59
|
+
tbl = table_name.to_s.upcase
|
|
60
|
+
|
|
61
|
+
sql = <<~SQL
|
|
62
|
+
SELECT KC.COLUMN_NAME
|
|
63
|
+
FROM QSYS2.SYSKEYCST KC
|
|
64
|
+
INNER JOIN QSYS2.SYSCST CST
|
|
65
|
+
ON KC.CONSTRAINT_SCHEMA = CST.CONSTRAINT_SCHEMA
|
|
66
|
+
AND KC.CONSTRAINT_NAME = CST.CONSTRAINT_NAME
|
|
67
|
+
WHERE CST.TABLE_SCHEMA = '#{schema}'
|
|
68
|
+
AND CST.TABLE_NAME = '#{tbl}'
|
|
69
|
+
AND CST.CONSTRAINT_TYPE = 'PRIMARY KEY'
|
|
70
|
+
ORDER BY KC.ORDINAL_POSITION
|
|
71
|
+
SQL
|
|
72
|
+
|
|
73
|
+
query_values(sql, "SCHEMA").map { |name| name.strip.downcase }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def indexes(table_name)
|
|
77
|
+
schema = @config[:schema]&.upcase
|
|
78
|
+
tbl = table_name.to_s.upcase
|
|
79
|
+
|
|
80
|
+
sql = <<~SQL
|
|
81
|
+
SELECT
|
|
82
|
+
IX.INDEX_NAME,
|
|
83
|
+
IX.IS_UNIQUE,
|
|
84
|
+
KC.COLUMN_NAME,
|
|
85
|
+
KC.ORDINAL_POSITION,
|
|
86
|
+
KC.ORDERING
|
|
87
|
+
FROM QSYS2.SYSINDEXES IX
|
|
88
|
+
INNER JOIN QSYS2.SYSKEYS KC
|
|
89
|
+
ON IX.INDEX_SCHEMA = KC.INDEX_SCHEMA
|
|
90
|
+
AND IX.INDEX_NAME = KC.INDEX_NAME
|
|
91
|
+
WHERE IX.TABLE_SCHEMA = '#{schema}'
|
|
92
|
+
AND IX.TABLE_NAME = '#{tbl}'
|
|
93
|
+
AND IX.INDEX_NAME NOT IN (
|
|
94
|
+
SELECT CONSTRAINT_NAME FROM QSYS2.SYSCST
|
|
95
|
+
WHERE TABLE_SCHEMA = '#{schema}'
|
|
96
|
+
AND TABLE_NAME = '#{tbl}'
|
|
97
|
+
AND CONSTRAINT_TYPE = 'PRIMARY KEY'
|
|
98
|
+
)
|
|
99
|
+
ORDER BY IX.INDEX_NAME, KC.ORDINAL_POSITION
|
|
100
|
+
SQL
|
|
101
|
+
|
|
102
|
+
result = internal_exec_query(sql, "SCHEMA")
|
|
103
|
+
|
|
104
|
+
indexes_hash = {}
|
|
105
|
+
result.each do |row|
|
|
106
|
+
idx_name = row["index_name"].strip.downcase
|
|
107
|
+
indexes_hash[idx_name] ||= {
|
|
108
|
+
unique: row["is_unique"]&.strip == "U",
|
|
109
|
+
columns: [],
|
|
110
|
+
orders: {}
|
|
111
|
+
}
|
|
112
|
+
col = row["column_name"].strip.downcase
|
|
113
|
+
indexes_hash[idx_name][:columns] << col
|
|
114
|
+
if row["ordering"]&.strip == "D"
|
|
115
|
+
indexes_hash[idx_name][:orders][col] = :desc
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
indexes_hash.map do |name, data|
|
|
120
|
+
IndexDefinition.new(
|
|
121
|
+
table_name.to_s,
|
|
122
|
+
name,
|
|
123
|
+
data[:unique],
|
|
124
|
+
data[:columns],
|
|
125
|
+
orders: data[:orders].presence || {}
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def foreign_keys(table_name)
|
|
131
|
+
schema = @config[:schema]&.upcase
|
|
132
|
+
tbl = table_name.to_s.upcase
|
|
133
|
+
|
|
134
|
+
sql = <<~SQL
|
|
135
|
+
SELECT
|
|
136
|
+
RC.CONSTRAINT_NAME AS FK_NAME,
|
|
137
|
+
KC.COLUMN_NAME,
|
|
138
|
+
RC.UNIQUE_CONSTRAINT_SCHEMA AS REF_SCHEMA,
|
|
139
|
+
UC.TABLE_NAME AS REFERENCED_TABLE_NAME,
|
|
140
|
+
RKC.COLUMN_NAME AS REFERENCED_COLUMN_NAME,
|
|
141
|
+
CST.DELETE_RULE,
|
|
142
|
+
CST.UPDATE_RULE
|
|
143
|
+
FROM QSYS2.SYSREFCST RC
|
|
144
|
+
INNER JOIN QSYS2.SYSCST CST
|
|
145
|
+
ON RC.CONSTRAINT_SCHEMA = CST.CONSTRAINT_SCHEMA
|
|
146
|
+
AND RC.CONSTRAINT_NAME = CST.CONSTRAINT_NAME
|
|
147
|
+
INNER JOIN QSYS2.SYSKEYCST KC
|
|
148
|
+
ON RC.CONSTRAINT_SCHEMA = KC.CONSTRAINT_SCHEMA
|
|
149
|
+
AND RC.CONSTRAINT_NAME = KC.CONSTRAINT_NAME
|
|
150
|
+
INNER JOIN QSYS2.SYSCST UC
|
|
151
|
+
ON RC.UNIQUE_CONSTRAINT_SCHEMA = UC.CONSTRAINT_SCHEMA
|
|
152
|
+
AND RC.UNIQUE_CONSTRAINT_NAME = UC.CONSTRAINT_NAME
|
|
153
|
+
INNER JOIN QSYS2.SYSKEYCST RKC
|
|
154
|
+
ON RC.UNIQUE_CONSTRAINT_SCHEMA = RKC.CONSTRAINT_SCHEMA
|
|
155
|
+
AND RC.UNIQUE_CONSTRAINT_NAME = RKC.CONSTRAINT_NAME
|
|
156
|
+
AND KC.ORDINAL_POSITION = RKC.ORDINAL_POSITION
|
|
157
|
+
WHERE CST.TABLE_SCHEMA = '#{schema}'
|
|
158
|
+
AND CST.TABLE_NAME = '#{tbl}'
|
|
159
|
+
ORDER BY RC.CONSTRAINT_NAME, KC.ORDINAL_POSITION
|
|
160
|
+
SQL
|
|
161
|
+
|
|
162
|
+
result = internal_exec_query(sql, "SCHEMA")
|
|
163
|
+
|
|
164
|
+
fk_hash = {}
|
|
165
|
+
result.each do |row|
|
|
166
|
+
fk_name = row["fk_name"]&.strip
|
|
167
|
+
fk_hash[fk_name] ||= {
|
|
168
|
+
from_columns: [],
|
|
169
|
+
to_table: row["referenced_table_name"]&.strip&.downcase,
|
|
170
|
+
to_columns: [],
|
|
171
|
+
on_delete: extract_fk_action(row["delete_rule"]),
|
|
172
|
+
on_update: extract_fk_action(row["update_rule"]),
|
|
173
|
+
}
|
|
174
|
+
fk_hash[fk_name][:from_columns] << row["column_name"]&.strip&.downcase
|
|
175
|
+
fk_hash[fk_name][:to_columns] << row["referenced_column_name"]&.strip&.downcase
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
fk_hash.map do |name, data|
|
|
179
|
+
options = {
|
|
180
|
+
name: name,
|
|
181
|
+
column: data[:from_columns].size == 1 ? data[:from_columns].first : data[:from_columns],
|
|
182
|
+
primary_key: data[:to_columns].size == 1 ? data[:to_columns].first : data[:to_columns],
|
|
183
|
+
on_delete: data[:on_delete],
|
|
184
|
+
on_update: data[:on_update],
|
|
185
|
+
}
|
|
186
|
+
ForeignKeyDefinition.new(table_name.to_s, data[:to_table], options)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# -- DDL Methods --
|
|
191
|
+
|
|
192
|
+
def rename_table(table_name, new_name, **)
|
|
193
|
+
execute("RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}")
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def drop_table(table_name, **options)
|
|
197
|
+
execute("DROP TABLE #{quote_table_name(table_name)}")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def change_column(table_name, column_name, type, **options)
|
|
201
|
+
sql_type = type_to_sql(type, **options.slice(:limit, :precision, :scale))
|
|
202
|
+
execute("ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DATA TYPE #{sql_type}")
|
|
203
|
+
|
|
204
|
+
if options.key?(:null)
|
|
205
|
+
change_column_null(table_name, column_name, options[:null])
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
if options.key?(:default)
|
|
209
|
+
change_column_default(table_name, column_name, options[:default])
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def change_column_default(table_name, column_name, default_or_changes)
|
|
214
|
+
default = extract_new_default_value(default_or_changes)
|
|
215
|
+
if default.nil?
|
|
216
|
+
execute("ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} DROP DEFAULT")
|
|
217
|
+
else
|
|
218
|
+
execute("ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}")
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def change_column_null(table_name, column_name, null, default = nil)
|
|
223
|
+
unless null || default.nil?
|
|
224
|
+
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)} = #{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
if null
|
|
228
|
+
execute("ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} DROP NOT NULL")
|
|
229
|
+
else
|
|
230
|
+
execute("ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET NOT NULL")
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def rename_column(table_name, column_name, new_column_name)
|
|
235
|
+
execute("ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}")
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def add_index(table_name, column_name, **options)
|
|
239
|
+
index_name = options[:name] || index_name(table_name, column_name)
|
|
240
|
+
unique = options[:unique] ? "UNIQUE " : ""
|
|
241
|
+
|
|
242
|
+
columns = Array(column_name).map do |col|
|
|
243
|
+
order = options[:order].is_a?(Hash) ? options[:order][col] : nil
|
|
244
|
+
"#{quote_column_name(col)}#{" DESC" if order.to_s == "desc"}"
|
|
245
|
+
end.join(", ")
|
|
246
|
+
|
|
247
|
+
execute("CREATE #{unique}INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{columns})")
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def remove_index(table_name, column_name = nil, **options)
|
|
251
|
+
index_name = options[:name] || index_name(table_name, column_name)
|
|
252
|
+
execute("DROP INDEX #{quote_column_name(index_name)}")
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def type_to_sql(type, limit: nil, precision: nil, scale: nil, **)
|
|
256
|
+
native = native_database_types[type.to_sym]
|
|
257
|
+
return type.to_s unless native
|
|
258
|
+
|
|
259
|
+
sql_type = native.is_a?(Hash) ? native[:name] : native
|
|
260
|
+
|
|
261
|
+
case type.to_sym
|
|
262
|
+
when :string, :char
|
|
263
|
+
limit ||= native[:limit] if native.is_a?(Hash)
|
|
264
|
+
limit ? "#{sql_type}(#{limit})" : sql_type
|
|
265
|
+
when :decimal
|
|
266
|
+
if precision
|
|
267
|
+
scale ? "#{sql_type}(#{precision},#{scale})" : "#{sql_type}(#{precision})"
|
|
268
|
+
else
|
|
269
|
+
sql_type
|
|
270
|
+
end
|
|
271
|
+
when :integer
|
|
272
|
+
if limit
|
|
273
|
+
case limit
|
|
274
|
+
when 1, 2 then "SMALLINT"
|
|
275
|
+
when 3, 4 then "INTEGER"
|
|
276
|
+
when 5..8 then "BIGINT"
|
|
277
|
+
else sql_type
|
|
278
|
+
end
|
|
279
|
+
else
|
|
280
|
+
sql_type
|
|
281
|
+
end
|
|
282
|
+
when :text, :binary, :float, :date, :time, :datetime, :timestamp, :boolean, :bigint
|
|
283
|
+
sql_type
|
|
284
|
+
else
|
|
285
|
+
sql_type
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def schema_creation
|
|
290
|
+
Peasys::SchemaCreation.new(self)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def create_table_definition(name, **options)
|
|
294
|
+
Peasys::TableDefinition.new(self, name, **options)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Tells Rails to use our custom SchemaDumper
|
|
298
|
+
def schema_dumper_class
|
|
299
|
+
Peasys::SchemaDumper
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# -- Rails internal tables --
|
|
303
|
+
|
|
304
|
+
# Ensures the schema_migrations table exists with DB2-compatible types.
|
|
305
|
+
def create_schema_migrations_table
|
|
306
|
+
unless table_exists?("schema_migrations")
|
|
307
|
+
execute(<<~SQL)
|
|
308
|
+
CREATE TABLE #{quote_table_name("schema_migrations")} (
|
|
309
|
+
"VERSION" VARCHAR(255) NOT NULL PRIMARY KEY
|
|
310
|
+
)
|
|
311
|
+
SQL
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Ensures the ar_internal_metadata table exists with DB2-compatible types.
|
|
316
|
+
def create_internal_metadata_table
|
|
317
|
+
unless table_exists?("ar_internal_metadata")
|
|
318
|
+
execute(<<~SQL)
|
|
319
|
+
CREATE TABLE #{quote_table_name("ar_internal_metadata")} (
|
|
320
|
+
"KEY" VARCHAR(255) NOT NULL PRIMARY KEY,
|
|
321
|
+
"VALUE" VARCHAR(255),
|
|
322
|
+
"CREATED_AT" TIMESTAMP NOT NULL,
|
|
323
|
+
"UPDATED_AT" TIMESTAMP NOT NULL
|
|
324
|
+
)
|
|
325
|
+
SQL
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
private
|
|
330
|
+
|
|
331
|
+
def column_definitions(table_name)
|
|
332
|
+
schema = @config[:schema]&.upcase
|
|
333
|
+
tbl = table_name.to_s.upcase
|
|
334
|
+
|
|
335
|
+
sql = <<~SQL
|
|
336
|
+
SELECT
|
|
337
|
+
COLUMN_NAME,
|
|
338
|
+
DATA_TYPE,
|
|
339
|
+
LENGTH AS COLUMN_SIZE,
|
|
340
|
+
NUMERIC_SCALE,
|
|
341
|
+
NUMERIC_PRECISION,
|
|
342
|
+
IS_NULLABLE,
|
|
343
|
+
COLUMN_DEFAULT,
|
|
344
|
+
HAS_DEFAULT,
|
|
345
|
+
IS_IDENTITY,
|
|
346
|
+
ORDINAL_POSITION,
|
|
347
|
+
COLUMN_TEXT AS COLUMN_COMMENT,
|
|
348
|
+
CCSID
|
|
349
|
+
FROM QSYS2.SYSCOLUMNS
|
|
350
|
+
WHERE TABLE_SCHEMA = '#{schema}'
|
|
351
|
+
AND TABLE_NAME = '#{tbl}'
|
|
352
|
+
ORDER BY ORDINAL_POSITION
|
|
353
|
+
SQL
|
|
354
|
+
|
|
355
|
+
internal_exec_query(sql, "SCHEMA").to_a
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def new_column_from_field(table_name, field, _definitions)
|
|
359
|
+
name = field["column_name"]&.strip&.downcase
|
|
360
|
+
sql_type = field["data_type"]&.strip
|
|
361
|
+
default = field["column_default"]&.strip
|
|
362
|
+
null = field["is_nullable"]&.strip == "Y"
|
|
363
|
+
is_identity = field["is_identity"]&.strip == "YES"
|
|
364
|
+
precision = field["numeric_precision"]
|
|
365
|
+
scale = field["numeric_scale"]
|
|
366
|
+
limit = field["column_size"]
|
|
367
|
+
comment = field["column_comment"]&.strip
|
|
368
|
+
|
|
369
|
+
full_sql_type = build_full_sql_type(sql_type, limit, precision, scale)
|
|
370
|
+
type_metadata = fetch_type_metadata(full_sql_type)
|
|
371
|
+
|
|
372
|
+
default_value = extract_value_from_default(default)
|
|
373
|
+
default_function = extract_default_function(default, default_value)
|
|
374
|
+
|
|
375
|
+
ActiveRecord::ConnectionAdapters::Column.new(
|
|
376
|
+
name,
|
|
377
|
+
default_value,
|
|
378
|
+
type_metadata,
|
|
379
|
+
null,
|
|
380
|
+
default_function,
|
|
381
|
+
comment: comment.presence
|
|
382
|
+
)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def build_full_sql_type(sql_type, limit, precision, scale)
|
|
386
|
+
case sql_type&.upcase
|
|
387
|
+
when "VARCHAR", "CHAR", "CHARACTER", "CHARACTER VARYING", "VARGRAPHIC", "GRAPHIC"
|
|
388
|
+
"#{sql_type}(#{limit})"
|
|
389
|
+
when "DECIMAL", "NUMERIC"
|
|
390
|
+
"#{sql_type}(#{precision},#{scale})"
|
|
391
|
+
else
|
|
392
|
+
sql_type.to_s
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def extract_value_from_default(default)
|
|
397
|
+
return nil if default.nil? || default.empty?
|
|
398
|
+
|
|
399
|
+
case default
|
|
400
|
+
when /\ANULL\z/i then nil
|
|
401
|
+
when /\A'(.*)'\z/m then $1.gsub("''", "'")
|
|
402
|
+
when /\A(-?\d+(\.\d*)?)\z/ then $1
|
|
403
|
+
when /\ACURRENT_TIMESTAMP\z/i,
|
|
404
|
+
/\ACURRENT_DATE\z/i,
|
|
405
|
+
/\ACURRENT_TIME\z/i then nil
|
|
406
|
+
else default
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def extract_default_function(default, default_value)
|
|
411
|
+
return nil if default.nil? || default.empty?
|
|
412
|
+
|
|
413
|
+
if default_value.nil? && default =~ /\A(CURRENT_TIMESTAMP|CURRENT_DATE|CURRENT_TIME)\z/i
|
|
414
|
+
default
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def extract_fk_action(rule)
|
|
419
|
+
case rule&.strip
|
|
420
|
+
when "CASCADE" then :cascade
|
|
421
|
+
when "SET NULL" then :nullify
|
|
422
|
+
when "SET DEFAULT" then :default
|
|
423
|
+
when "RESTRICT" then :restrict
|
|
424
|
+
when "NO ACTION" then nil
|
|
425
|
+
else nil
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
end
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record/connection_adapters/abstract_adapter"
|
|
4
|
+
require_relative "peasys/quoting"
|
|
5
|
+
require_relative "peasys/database_statements"
|
|
6
|
+
require_relative "peasys/schema_statements"
|
|
7
|
+
require_relative "peasys/schema_creation"
|
|
8
|
+
require_relative "peasys/schema_definitions"
|
|
9
|
+
require_relative "peasys/column"
|
|
10
|
+
require_relative "peasys/schema_dumper"
|
|
11
|
+
|
|
12
|
+
require "arel/visitors/peasys"
|
|
13
|
+
|
|
14
|
+
module ActiveRecord
|
|
15
|
+
module ConnectionAdapters
|
|
16
|
+
class PeasysAdapter < AbstractAdapter
|
|
17
|
+
ADAPTER_NAME = "Peasys"
|
|
18
|
+
|
|
19
|
+
include Peasys::Quoting
|
|
20
|
+
include Peasys::DatabaseStatements
|
|
21
|
+
include Peasys::SchemaStatements
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
def new_client(config)
|
|
25
|
+
PeaClient.new(
|
|
26
|
+
config[:ip_address].to_s,
|
|
27
|
+
config[:partition_name].to_s,
|
|
28
|
+
config[:port].to_i,
|
|
29
|
+
config[:username].to_s,
|
|
30
|
+
config[:password].to_s,
|
|
31
|
+
config[:id_client].to_s,
|
|
32
|
+
config.fetch(:online_version, true),
|
|
33
|
+
config.fetch(:retrieve_statistics, false)
|
|
34
|
+
)
|
|
35
|
+
rescue PeaConnexionError => e
|
|
36
|
+
raise ActiveRecord::ConnectionNotEstablished, e.message
|
|
37
|
+
rescue PeaInvalidCredentialsError => e
|
|
38
|
+
raise ActiveRecord::ConnectionNotEstablished, e.message
|
|
39
|
+
rescue PeaInvalidLicenseKeyError => e
|
|
40
|
+
raise ActiveRecord::ConnectionNotEstablished, e.message
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def initialize(...)
|
|
45
|
+
super
|
|
46
|
+
|
|
47
|
+
@connection_parameters = @config
|
|
48
|
+
@config[:schema] ||= @config[:username]&.upcase
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# -- Adapter identification --
|
|
52
|
+
|
|
53
|
+
def adapter_name
|
|
54
|
+
ADAPTER_NAME
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# -- Feature flags --
|
|
58
|
+
|
|
59
|
+
def supports_migrations?; true end
|
|
60
|
+
def supports_primary_key?; true end
|
|
61
|
+
def supports_foreign_keys?; true end
|
|
62
|
+
def supports_views?; true end
|
|
63
|
+
def supports_datetime_with_precision?; true end
|
|
64
|
+
def supports_ddl_transactions?; false end
|
|
65
|
+
def supports_savepoints?; false end
|
|
66
|
+
def supports_transaction_isolation?; false end
|
|
67
|
+
def supports_explain?; false end
|
|
68
|
+
def supports_index_sort_order?; true end
|
|
69
|
+
def supports_insert_returning?; false end
|
|
70
|
+
def supports_common_table_expressions?; true end
|
|
71
|
+
def supports_lazy_transactions?; true end
|
|
72
|
+
def supports_check_constraints?; true end
|
|
73
|
+
def supports_comments?; true end
|
|
74
|
+
def supports_insert_on_duplicate_skip?; false end
|
|
75
|
+
def supports_insert_on_duplicate_update?; false end
|
|
76
|
+
|
|
77
|
+
# -- Schema Dumper --
|
|
78
|
+
|
|
79
|
+
def self.database_exists?(config)
|
|
80
|
+
true # Assume the IBM i database always exists if configured
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# -- Connection management --
|
|
84
|
+
|
|
85
|
+
def active?
|
|
86
|
+
@raw_connection&.connexion_status == 1
|
|
87
|
+
rescue
|
|
88
|
+
false
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def disconnect!
|
|
92
|
+
super
|
|
93
|
+
@raw_connection&.disconnect
|
|
94
|
+
rescue
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# -- Type mapping --
|
|
99
|
+
|
|
100
|
+
NATIVE_DATABASE_TYPES = {
|
|
101
|
+
primary_key: "INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY",
|
|
102
|
+
string: { name: "VARCHAR", limit: 255 },
|
|
103
|
+
text: { name: "CLOB" },
|
|
104
|
+
integer: { name: "INTEGER" },
|
|
105
|
+
bigint: { name: "BIGINT" },
|
|
106
|
+
float: { name: "DOUBLE" },
|
|
107
|
+
decimal: { name: "DECIMAL" },
|
|
108
|
+
datetime: { name: "TIMESTAMP" },
|
|
109
|
+
timestamp: { name: "TIMESTAMP" },
|
|
110
|
+
time: { name: "TIME" },
|
|
111
|
+
date: { name: "DATE" },
|
|
112
|
+
binary: { name: "BLOB" },
|
|
113
|
+
boolean: { name: "SMALLINT" },
|
|
114
|
+
json: { name: "CLOB" },
|
|
115
|
+
}.freeze
|
|
116
|
+
|
|
117
|
+
def native_database_types
|
|
118
|
+
NATIVE_DATABASE_TYPES
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
class << self
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def initialize_type_map(m)
|
|
125
|
+
super
|
|
126
|
+
|
|
127
|
+
# DB2-specific type aliases
|
|
128
|
+
m.alias_type %r(clob)i, "text"
|
|
129
|
+
m.alias_type %r(dbclob)i, "text"
|
|
130
|
+
m.alias_type %r(blob)i, "binary"
|
|
131
|
+
m.alias_type %r(graphic)i, "string"
|
|
132
|
+
m.alias_type %r(vargraphic)i, "string"
|
|
133
|
+
m.alias_type %r(timestamp)i, "datetime"
|
|
134
|
+
m.alias_type %r(timestmp)i, "datetime"
|
|
135
|
+
m.alias_type %r(double)i, "float"
|
|
136
|
+
m.alias_type %r(real)i, "float"
|
|
137
|
+
m.alias_type %r(numeric)i, "decimal"
|
|
138
|
+
m.alias_type %r(decfloat)i, "decimal"
|
|
139
|
+
|
|
140
|
+
m.register_type %r(smallint)i, Type::Integer.new(limit: 2)
|
|
141
|
+
m.register_type %r(\Ainteger\z)i, Type::Integer.new(limit: 4)
|
|
142
|
+
m.register_type %r(bigint)i, Type::Integer.new(limit: 8)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
TYPE_MAP = Type::TypeMap.new.tap { |m| initialize_type_map(m) }
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def type_map
|
|
151
|
+
TYPE_MAP
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def connect
|
|
155
|
+
@raw_connection = self.class.new_client(@connection_parameters)
|
|
156
|
+
rescue PeaConnexionError => e
|
|
157
|
+
raise ConnectionNotEstablished, e.message
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def reconnect
|
|
161
|
+
@raw_connection&.disconnect rescue nil
|
|
162
|
+
connect
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def arel_visitor
|
|
166
|
+
Arel::Visitors::Peasys.new(self)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def configure_connection
|
|
170
|
+
if @config[:schema]
|
|
171
|
+
with_raw_connection do |conn|
|
|
172
|
+
conn.execute_sql("SET SCHEMA #{@config[:schema].upcase}")
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def translate_exception(exception, message:, sql:, binds:)
|
|
178
|
+
case exception
|
|
179
|
+
when PeaQueryerror
|
|
180
|
+
msg = exception.message
|
|
181
|
+
if msg =~ /SQL0803|23505/ # Unique constraint violation
|
|
182
|
+
RecordNotUnique.new(message, sql: sql, binds: binds, connection_pool: @pool)
|
|
183
|
+
elsif msg =~ /SQL0530|SQL0532|23503/ # FK violation
|
|
184
|
+
InvalidForeignKey.new(message, sql: sql, binds: binds, connection_pool: @pool)
|
|
185
|
+
elsif msg =~ /SQL0407|23502/ # NOT NULL violation
|
|
186
|
+
NotNullViolation.new(message, sql: sql, binds: binds, connection_pool: @pool)
|
|
187
|
+
elsif msg =~ /SQL0433/ # Value too long
|
|
188
|
+
ValueTooLong.new(message, sql: sql, binds: binds, connection_pool: @pool)
|
|
189
|
+
else
|
|
190
|
+
StatementInvalid.new(message, sql: sql, binds: binds, connection_pool: @pool)
|
|
191
|
+
end
|
|
192
|
+
when PeaConnexionError
|
|
193
|
+
ConnectionNotEstablished.new(message, connection_pool: @pool)
|
|
194
|
+
else
|
|
195
|
+
super
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Rails 7.2 adapter registration
|
|
203
|
+
ActiveRecord::ConnectionAdapters.register(
|
|
204
|
+
"peasys",
|
|
205
|
+
"ActiveRecord::ConnectionAdapters::PeasysAdapter",
|
|
206
|
+
"active_record/connection_adapters/peasys_adapter"
|
|
207
|
+
)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arel
|
|
4
|
+
module Visitors
|
|
5
|
+
class Peasys < Arel::Visitors::ToSql
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
# DB2 uses FETCH FIRST n ROWS ONLY instead of LIMIT
|
|
9
|
+
def visit_Arel_Nodes_Limit(o, collector)
|
|
10
|
+
collector << " FETCH FIRST "
|
|
11
|
+
visit o.expr, collector
|
|
12
|
+
collector << " ROWS ONLY"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# DB2 uses OFFSET n ROWS
|
|
16
|
+
def visit_Arel_Nodes_Offset(o, collector)
|
|
17
|
+
collector << " OFFSET "
|
|
18
|
+
visit o.expr, collector
|
|
19
|
+
collector << " ROWS"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# DB2 boolean literals: 1/0 instead of TRUE/FALSE
|
|
23
|
+
def visit_Arel_Nodes_True(o, collector)
|
|
24
|
+
collector << "1"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def visit_Arel_Nodes_False(o, collector)
|
|
28
|
+
collector << "0"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# DB2 FOR UPDATE syntax
|
|
32
|
+
def visit_Arel_Nodes_Lock(o, collector)
|
|
33
|
+
case o.expr
|
|
34
|
+
when true
|
|
35
|
+
collector << " FOR UPDATE WITH RS"
|
|
36
|
+
when String
|
|
37
|
+
collector << " " << o.expr
|
|
38
|
+
else
|
|
39
|
+
collector
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# DB2 string concatenation uses ||
|
|
44
|
+
def visit_Arel_Nodes_Concat(o, collector)
|
|
45
|
+
collector << "("
|
|
46
|
+
visit o.left, collector
|
|
47
|
+
collector << " || "
|
|
48
|
+
visit o.right, collector
|
|
49
|
+
collector << ")"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|