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.
@@ -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