database-model-generator 0.6.1 → 0.7.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,304 @@
1
+ require_relative '../../database_model_generator'
2
+
3
+ begin
4
+ require 'pg'
5
+ rescue LoadError
6
+ # pg not available - PostgreSQL support will be disabled
7
+ end
8
+
9
+ module Postgresql
10
+ module Model
11
+ class Generator < DatabaseModel::Generator::Base
12
+ VERSION = DatabaseModel::Generator::VERSION
13
+
14
+ private
15
+
16
+ def validate_connection(connection)
17
+ unless defined?(PG) && connection.is_a?(PG::Connection)
18
+ raise ArgumentError, "Connection must be a PG::Connection object. Install 'pg' gem for PostgreSQL support."
19
+ end
20
+ end
21
+
22
+ def normalize_table_name(table)
23
+ table.downcase # PostgreSQL uses lowercase for table names by convention
24
+ end
25
+
26
+ def check_table_exists(table)
27
+ begin
28
+ sql = "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name = $1"
29
+ result = @connection.exec_params(sql, [table.downcase])
30
+ row = result.first
31
+ result.clear
32
+ row && row['count'].to_i > 0
33
+ rescue => e
34
+ puts "Error checking table existence: #{e.message}"
35
+ false
36
+ end
37
+ end
38
+
39
+ def get_column_info
40
+ begin
41
+ sql = <<~SQL
42
+ SELECT
43
+ column_name,
44
+ data_type,
45
+ character_maximum_length,
46
+ numeric_precision,
47
+ numeric_scale,
48
+ is_nullable,
49
+ column_default,
50
+ udt_name
51
+ FROM information_schema.columns
52
+ WHERE table_schema = 'public' AND table_name = $1
53
+ ORDER BY ordinal_position
54
+ SQL
55
+
56
+ result = @connection.exec_params(sql, [@table])
57
+ @column_info = []
58
+
59
+ result.each do |row|
60
+ @column_info << PostgresqlColumnInfo.new(row)
61
+ end
62
+
63
+ result.clear
64
+ rescue => e
65
+ raise "Error retrieving column information: #{e.message}"
66
+ end
67
+ end
68
+
69
+ def get_primary_keys
70
+ begin
71
+ sql = <<~SQL
72
+ SELECT a.attname AS column_name
73
+ FROM pg_index i
74
+ JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
75
+ WHERE i.indrelid = $1::regclass AND i.indisprimary
76
+ ORDER BY a.attnum
77
+ SQL
78
+
79
+ result = @connection.exec_params(sql, [@table])
80
+ @primary_keys = []
81
+
82
+ result.each do |row|
83
+ @primary_keys << row['column_name']
84
+ end
85
+
86
+ result.clear
87
+ rescue => e
88
+ raise "Error retrieving primary keys: #{e.message}"
89
+ end
90
+ end
91
+
92
+ def get_foreign_keys
93
+ begin
94
+ sql = <<~SQL
95
+ SELECT
96
+ kcu.column_name
97
+ FROM information_schema.table_constraints tc
98
+ JOIN information_schema.key_column_usage kcu
99
+ ON tc.constraint_name = kcu.constraint_name
100
+ AND tc.table_schema = kcu.table_schema
101
+ WHERE tc.table_schema = 'public'
102
+ AND tc.table_name = $1
103
+ AND tc.constraint_type = 'FOREIGN KEY'
104
+ SQL
105
+
106
+ result = @connection.exec_params(sql, [@table])
107
+ @foreign_keys = []
108
+
109
+ result.each do |row|
110
+ @foreign_keys << row['column_name']
111
+ end
112
+
113
+ result.clear
114
+ rescue => e
115
+ raise "Error retrieving foreign keys: #{e.message}"
116
+ end
117
+ end
118
+
119
+ def get_constraints
120
+ begin
121
+ sql = <<~SQL
122
+ SELECT
123
+ tc.constraint_name,
124
+ tc.constraint_type,
125
+ kcu.column_name,
126
+ cc.check_clause
127
+ FROM information_schema.table_constraints tc
128
+ LEFT JOIN information_schema.key_column_usage kcu
129
+ ON tc.constraint_name = kcu.constraint_name
130
+ AND tc.table_schema = kcu.table_schema
131
+ LEFT JOIN information_schema.check_constraints cc
132
+ ON tc.constraint_name = cc.constraint_name
133
+ AND tc.constraint_schema = cc.constraint_schema
134
+ WHERE tc.table_schema = 'public'
135
+ AND tc.table_name = $1
136
+ SQL
137
+
138
+ result = @connection.exec_params(sql, [@table])
139
+ @constraints = []
140
+
141
+ result.each do |row|
142
+ @constraints << {
143
+ 'CONSTRAINT_NAME' => row['constraint_name'],
144
+ 'CONSTRAINT_TYPE' => row['constraint_type'],
145
+ 'COLUMN_NAME' => row['column_name'],
146
+ 'CHECK_CLAUSE' => row['check_clause']
147
+ }
148
+ end
149
+
150
+ result.clear
151
+ rescue => e
152
+ raise "Error retrieving constraints: #{e.message}"
153
+ end
154
+ end
155
+
156
+ def get_dependencies
157
+ begin
158
+ sql = <<~SQL
159
+ SELECT
160
+ dependent_ns.nspname AS dependent_schema,
161
+ dependent_view.relname AS dependent_view,
162
+ source_table.relname AS source_table
163
+ FROM pg_depend
164
+ JOIN pg_rewrite ON pg_depend.objid = pg_rewrite.oid
165
+ JOIN pg_class AS dependent_view ON pg_rewrite.ev_class = dependent_view.oid
166
+ JOIN pg_class AS source_table ON pg_depend.refobjid = source_table.oid
167
+ JOIN pg_namespace dependent_ns ON dependent_ns.oid = dependent_view.relnamespace
168
+ WHERE source_table.relname = $1
169
+ AND dependent_ns.nspname = 'public'
170
+ AND source_table.relname != dependent_view.relname
171
+ SQL
172
+
173
+ result = @connection.exec_params(sql, [@table])
174
+ @dependencies = []
175
+
176
+ result.each do |row|
177
+ @dependencies << {
178
+ 'NAME' => row['dependent_view'],
179
+ 'TYPE' => 'VIEW'
180
+ }
181
+ end
182
+
183
+ result.clear
184
+ rescue => e
185
+ # Dependencies query may fail on some PostgreSQL versions, continue without error
186
+ @dependencies = []
187
+ end
188
+ end
189
+
190
+ def format_constraint_type(constraint)
191
+ case constraint['CONSTRAINT_TYPE']
192
+ when 'PRIMARY KEY' then 'Primary Key'
193
+ when 'FOREIGN KEY' then 'Foreign Key'
194
+ when 'UNIQUE' then 'Unique'
195
+ when 'CHECK' then 'Check'
196
+ else constraint['CONSTRAINT_TYPE']
197
+ end
198
+ end
199
+
200
+ def get_constraint_column_name(constraint)
201
+ constraint['COLUMN_NAME']
202
+ end
203
+
204
+ def get_constraint_text(constraint)
205
+ constraint['CHECK_CLAUSE']
206
+ end
207
+
208
+ def is_string_type?(column)
209
+ column.data_type.downcase =~ /(char|varchar|character varying|text|bpchar)/
210
+ end
211
+
212
+ def is_date_type?(column)
213
+ column.data_type.downcase =~ /(date|time|timestamp)/
214
+ end
215
+
216
+ def is_text_type?(column)
217
+ column.data_type.downcase =~ /(varchar|character varying|text|char)/
218
+ end
219
+
220
+ def get_column_size(column)
221
+ column.character_maximum_length
222
+ end
223
+
224
+ def build_full_text_index_sql(column)
225
+ "CREATE INDEX idx_#{@table.downcase}_#{column.name.downcase}_text ON #{@table.downcase} USING GIN (to_tsvector('english', #{column.name.downcase}))"
226
+ end
227
+
228
+ def get_full_text_index_type
229
+ "PostgreSQL Full-Text Search (GIN Index)"
230
+ end
231
+
232
+ def find_fk_table(fk)
233
+ # PostgreSQL-specific logic for finding referenced table
234
+ # Try to get actual referenced table from foreign key constraints
235
+ begin
236
+ sql = <<~SQL
237
+ SELECT
238
+ ccu.table_name AS referenced_table
239
+ FROM information_schema.table_constraints tc
240
+ JOIN information_schema.key_column_usage kcu
241
+ ON tc.constraint_name = kcu.constraint_name
242
+ AND tc.table_schema = kcu.table_schema
243
+ JOIN information_schema.constraint_column_usage ccu
244
+ ON tc.constraint_name = ccu.constraint_name
245
+ AND tc.table_schema = ccu.table_schema
246
+ WHERE tc.table_schema = 'public'
247
+ AND tc.table_name = $1
248
+ AND tc.constraint_type = 'FOREIGN KEY'
249
+ AND kcu.column_name = $2
250
+ SQL
251
+
252
+ result = @connection.exec_params(sql, [@table, fk])
253
+ row = result.first
254
+ if row && row['referenced_table']
255
+ result.clear
256
+ return row['referenced_table'].downcase
257
+ end
258
+ result.clear
259
+ rescue => e
260
+ # Fall back to naming convention if query fails
261
+ end
262
+
263
+ # Fallback to naming convention
264
+ fk.gsub(/_id$/i, '').pluralize rescue "#{fk.gsub(/_id$/i, '')}s"
265
+ end
266
+
267
+ def disconnect
268
+ @connection.close if @connection && !@connection.finished?
269
+ end
270
+ end
271
+
272
+ # PostgreSQL column info wrapper
273
+ class PostgresqlColumnInfo
274
+ attr_reader :name, :data_type, :character_maximum_length, :numeric_precision, :numeric_scale, :nullable, :column_default, :udt_name
275
+
276
+ def initialize(row)
277
+ @name = row['column_name']
278
+ @data_type = row['data_type']
279
+ @character_maximum_length = row['character_maximum_length']&.to_i
280
+ @numeric_precision = row['numeric_precision']&.to_i
281
+ @numeric_scale = row['numeric_scale']&.to_i
282
+ @nullable = row['is_nullable'] == 'YES'
283
+ @column_default = row['column_default']
284
+ @udt_name = row['udt_name'] # PostgreSQL-specific user-defined type name
285
+ end
286
+
287
+ def nullable?
288
+ @nullable
289
+ end
290
+
291
+ def data_size
292
+ @character_maximum_length
293
+ end
294
+
295
+ def precision
296
+ @numeric_precision
297
+ end
298
+
299
+ def scale
300
+ @numeric_scale
301
+ end
302
+ end
303
+ end
304
+ end
@@ -0,0 +1,325 @@
1
+ require 'spec_helper'
2
+ require 'support/postgresql_connection'
3
+ require 'postgresql/model/generator'
4
+
5
+ RSpec.describe Postgresql::Model::Generator do
6
+ include_context 'PostgreSQL connection'
7
+
8
+ let(:generator) { described_class.new(connection) }
9
+
10
+ describe 'class information' do
11
+ it 'has the correct version number' do
12
+ expect(Postgresql::Model::Generator::VERSION).to eq('0.6.1')
13
+ end
14
+ end
15
+
16
+ describe '#initialize' do
17
+ it 'accepts a PG::Connection object' do
18
+ expect { described_class.new(connection) }.not_to raise_error
19
+ end
20
+
21
+ it 'sets up instance variables correctly' do
22
+ expect(generator.connection).to eq(connection)
23
+ expect(generator.constraints).to eq([])
24
+ expect(generator.primary_keys).to eq([])
25
+ expect(generator.foreign_keys).to eq([])
26
+ expect(generator.dependencies).to eq([])
27
+ expect(generator.belongs_to).to eq([])
28
+ expect(generator.column_info).to eq([])
29
+ expect(generator.table).to be_nil
30
+ expect(generator.model).to be_nil
31
+ end
32
+
33
+ it 'raises an error if connection is nil' do
34
+ expect { described_class.new(nil) }.to raise_error(ArgumentError, /Connection cannot be nil/)
35
+ end
36
+
37
+ it 'raises an error if connection is not a PG::Connection' do
38
+ expect { described_class.new("not a connection") }.to raise_error(ArgumentError, /must be a PG::Connection/)
39
+ end
40
+ end
41
+
42
+ describe '#generate' do
43
+ it 'responds to the generate method' do
44
+ expect(generator).to respond_to(:generate)
45
+ end
46
+
47
+ it 'works with a table name' do
48
+ expect { generator.generate('users') }.not_to raise_error
49
+ end
50
+
51
+ it 'raises an error for nil table name' do
52
+ expect { generator.generate(nil) }.to raise_error(ArgumentError, /Table name cannot be nil/)
53
+ end
54
+
55
+ it 'raises an error for empty table name' do
56
+ expect { generator.generate('') }.to raise_error(ArgumentError, /Table name cannot be nil/)
57
+ end
58
+
59
+ context 'when generating from the users table' do
60
+ before { generator.generate('users') }
61
+
62
+ it 'sets the table name in lowercase' do
63
+ expect(generator.table).to eq('users')
64
+ end
65
+
66
+ it 'generates a model name from the table name' do
67
+ expect(generator.model).to eq('User')
68
+ end
69
+
70
+ it 'sets view to false' do
71
+ expect(generator.view).to be false
72
+ end
73
+
74
+ it 'populates column information' do
75
+ expect(generator.column_info).to be_an(Array)
76
+ expect(generator.column_info).not_to be_empty
77
+
78
+ column_names = generator.column_info.map(&:name)
79
+ expect(column_names).to include('id')
80
+ expect(column_names).to include('username')
81
+ expect(column_names).to include('email')
82
+ expect(column_names).to include('status')
83
+ expect(column_names).to include('role')
84
+ end
85
+
86
+ it 'finds primary keys' do
87
+ expect(generator.primary_keys).to be_an(Array)
88
+ expect(generator.primary_keys).to include('id')
89
+ end
90
+
91
+ it 'populates constraints information' do
92
+ expect(generator.constraints).to be_an(Array)
93
+ expect(generator.constraints).not_to be_empty
94
+ end
95
+
96
+ it 'detects enum columns' do
97
+ expect(generator.has_enum_columns?).to be true
98
+ enum_column_names = generator.enum_column_names
99
+ expect(enum_column_names).to include('status')
100
+ expect(enum_column_names).to include('role')
101
+ end
102
+
103
+ it 'extracts enum values from constraints' do
104
+ generator.generate('users')
105
+ status_enum = generator.enum_columns.find { |e| e[:name] == 'status' }
106
+ expect(status_enum).not_to be_nil
107
+ expect(status_enum[:values]).to include('active', 'inactive', 'suspended', 'deleted')
108
+ end
109
+ end
110
+
111
+ context 'when generating from the posts table' do
112
+ before { generator.generate('posts') }
113
+
114
+ it 'sets the table name in lowercase' do
115
+ expect(generator.table).to eq('posts')
116
+ end
117
+
118
+ it 'generates the correct model name' do
119
+ expect(generator.model).to eq('Post')
120
+ end
121
+
122
+ it 'finds foreign keys' do
123
+ expect(generator.foreign_keys).to be_an(Array)
124
+ expect(generator.foreign_keys).to include('user_id')
125
+ end
126
+
127
+ it 'identifies belongs_to associations' do
128
+ expect(generator.belongs_to).to be_an(Array)
129
+ expect(generator.belongs_to).to include('users')
130
+ end
131
+
132
+ it 'detects multiple enum columns' do
133
+ enum_column_names = generator.enum_column_names
134
+ expect(enum_column_names).to include('status')
135
+ expect(enum_column_names).to include('priority')
136
+ end
137
+
138
+ it 'extracts enum values for status' do
139
+ status_enum = generator.enum_columns.find { |e| e[:name] == 'status' }
140
+ expect(status_enum).not_to be_nil
141
+ expect(status_enum[:values]).to include('draft', 'published', 'archived')
142
+ end
143
+
144
+ it 'extracts enum values for priority' do
145
+ priority_enum = generator.enum_columns.find { |e| e[:name] == 'priority' }
146
+ expect(priority_enum).not_to be_nil
147
+ expect(priority_enum[:values]).to include('low', 'medium', 'high', 'urgent')
148
+ end
149
+ end
150
+
151
+ context 'when generating from the comments table' do
152
+ before { generator.generate('comments') }
153
+
154
+ it 'detects polymorphic associations' do
155
+ expect(generator.has_polymorphic_associations?).to be true
156
+ expect(generator.polymorphic_association_names).to include('commentable')
157
+ end
158
+
159
+ it 'identifies polymorphic foreign keys and types' do
160
+ poly_assoc = generator.polymorphic_associations.first
161
+ expect(poly_assoc[:foreign_key]).to eq('commentable_id')
162
+ expect(poly_assoc[:foreign_type]).to eq('commentable_type')
163
+ end
164
+ end
165
+
166
+ context 'when generating from the post_tags table' do
167
+ before { generator.generate('post_tags') }
168
+
169
+ it 'detects composite primary keys' do
170
+ expect(generator.primary_keys).to be_an(Array)
171
+ expect(generator.primary_keys.length).to be >= 2
172
+ expect(generator.primary_keys).to include('post_id')
173
+ expect(generator.primary_keys).to include('tag_id')
174
+ end
175
+
176
+ it 'identifies multiple foreign keys' do
177
+ expect(generator.foreign_keys).to include('post_id')
178
+ expect(generator.foreign_keys).to include('tag_id')
179
+ end
180
+ end
181
+ end
182
+
183
+ describe '#table_exists?' do
184
+ it 'returns true for existing tables' do
185
+ generator.generate('users')
186
+ expect(generator.table_exists?).to be true
187
+ end
188
+
189
+ it 'returns false for non-existing tables' do
190
+ generator.generate('nonexistent_table')
191
+ expect(generator.table_exists?).to be false
192
+ end
193
+ end
194
+
195
+ describe '#generated?' do
196
+ it 'returns false before generate is called' do
197
+ expect(generator.generated?).to be false
198
+ end
199
+
200
+ it 'returns true after successful generation' do
201
+ generator.generate('users')
202
+ expect(generator.generated?).to be true
203
+ end
204
+ end
205
+
206
+ describe '#column_names' do
207
+ it 'returns an array of column names' do
208
+ generator.generate('users')
209
+ column_names = generator.column_names
210
+ expect(column_names).to be_an(Array)
211
+ expect(column_names).to include('id', 'username', 'email')
212
+ end
213
+ end
214
+
215
+ describe '#constraint_summary' do
216
+ it 'returns a hash of constraints by column' do
217
+ generator.generate('users')
218
+ summary = generator.constraint_summary
219
+ expect(summary).to be_a(Hash)
220
+ end
221
+ end
222
+
223
+ describe '#index_recommendations' do
224
+ it 'returns index recommendations' do
225
+ generator.generate('posts')
226
+ recommendations = generator.index_recommendations
227
+
228
+ expect(recommendations).to be_a(Hash)
229
+ expect(recommendations).to have_key(:foreign_keys)
230
+ expect(recommendations).to have_key(:date_queries)
231
+ expect(recommendations).to have_key(:status_enum)
232
+ expect(recommendations).to have_key(:composite)
233
+ expect(recommendations).to have_key(:full_text)
234
+ end
235
+
236
+ it 'recommends indexes for foreign keys' do
237
+ generator.generate('posts')
238
+ recommendations = generator.index_recommendations
239
+
240
+ fk_recommendations = recommendations[:foreign_keys]
241
+ expect(fk_recommendations).not_to be_empty
242
+
243
+ user_id_index = fk_recommendations.find { |r| r[:column] == 'user_id' }
244
+ expect(user_id_index).not_to be_nil
245
+ end
246
+
247
+ it 'recommends indexes for date columns' do
248
+ generator.generate('posts')
249
+ recommendations = generator.index_recommendations
250
+
251
+ date_recommendations = recommendations[:date_queries]
252
+ created_at_index = date_recommendations.find { |r| r[:column] == 'created_at' }
253
+ expect(created_at_index).not_to be_nil if date_recommendations.any?
254
+ end
255
+
256
+ it 'recommends full-text indexes for text columns' do
257
+ generator.generate('posts')
258
+ recommendations = generator.index_recommendations
259
+
260
+ fulltext_recommendations = recommendations[:full_text]
261
+ expect(fulltext_recommendations).to be_an(Array)
262
+ end
263
+ end
264
+
265
+ describe '#enum_definitions' do
266
+ it 'generates Rails enum definitions' do
267
+ generator.generate('users')
268
+ definitions = generator.enum_definitions
269
+
270
+ expect(definitions).to be_an(Array)
271
+ expect(definitions).not_to be_empty
272
+
273
+ # Check that definitions are in the correct format
274
+ definitions.each do |definition|
275
+ expect(definition).to match(/^enum \w+: \{.+\}$/)
276
+ end
277
+ end
278
+ end
279
+
280
+ describe '#polymorphic_has_many_suggestions' do
281
+ it 'suggests has_many associations for polymorphic parents' do
282
+ generator.generate('comments')
283
+ suggestions = generator.polymorphic_has_many_suggestions
284
+
285
+ expect(suggestions).to be_an(Array)
286
+ expect(suggestions).not_to be_empty if generator.has_polymorphic_associations?
287
+ end
288
+ end
289
+
290
+ describe '#model' do
291
+ it 'returns the active record model name' do
292
+ generator.generate('users')
293
+ expect(generator.model).to eq('User')
294
+ end
295
+
296
+ it 'removes trailing s from model names' do
297
+ generator.generate('posts')
298
+ expect(generator.model).to eq('Post')
299
+ end
300
+
301
+ it 'handles underscored table names' do
302
+ generator.generate('post_tags')
303
+ expect(generator.model).to eq('PostTag')
304
+ end
305
+ end
306
+
307
+ describe '#disconnect' do
308
+ it 'closes the database connection' do
309
+ conn_params = {
310
+ host: ENV['POSTGRES_HOST'] || 'localhost',
311
+ port: ENV['POSTGRES_PORT'] || '5432',
312
+ dbname: ENV['POSTGRES_DATABASE'] || 'test_db',
313
+ user: ENV['POSTGRES_USER'] || 'postgres'
314
+ }
315
+ conn_params[:password] = ENV['POSTGRES_PASSWORD'] if ENV['POSTGRES_PASSWORD']
316
+
317
+ test_conn = PG.connect(conn_params)
318
+
319
+ test_gen = described_class.new(test_conn)
320
+ test_gen.disconnect
321
+
322
+ expect(test_conn.finished?).to be true
323
+ end
324
+ end
325
+ end
@@ -0,0 +1,71 @@
1
+ require 'pg'
2
+
3
+ # Shared context for PostgreSQL database connection and test data
4
+ RSpec.shared_context 'PostgreSQL connection' do
5
+ before(:context) do
6
+ @pg_connection = begin
7
+ # In Docker environment, use the service name instead of localhost
8
+ host = ENV['POSTGRES_HOST'] || 'localhost'
9
+ port = ENV['POSTGRES_PORT'] || '5432'
10
+ database = ENV['POSTGRES_DATABASE'] || 'test_db'
11
+ user = ENV['POSTGRES_USER'] || 'postgres'
12
+ password = ENV['POSTGRES_PASSWORD']
13
+
14
+ conn_params = {
15
+ host: host,
16
+ port: port,
17
+ dbname: database,
18
+ user: user
19
+ }
20
+ conn_params[:password] = password if password
21
+
22
+ PG.connect(conn_params)
23
+ rescue PG::Error => e
24
+ warn "Database connection failed: #{e.message}"
25
+ warn "Make sure PostgreSQL database is running and accessible"
26
+ warn "Connection string: #{host}:#{port}/#{database}"
27
+ warn "User: #{user}"
28
+ raise e
29
+ end
30
+
31
+ # Ensure test data exists
32
+ setup_test_data
33
+ end
34
+
35
+ after(:context) do
36
+ # Cleanup test data if needed
37
+ cleanup_test_data
38
+
39
+ # Close the connection
40
+ @pg_connection&.close
41
+ end
42
+
43
+ # Use a method instead of let for context-level variables
44
+ def connection
45
+ @pg_connection
46
+ end
47
+
48
+ private
49
+
50
+ def setup_test_data
51
+ # The init-db.sql script should have already created the tables and data
52
+ # This is just a verification step
53
+ begin
54
+ result = @pg_connection.exec("SELECT COUNT(*) FROM users")
55
+ count = result.first['count'].to_i
56
+ result.clear
57
+
58
+ if count == 0
59
+ warn "Warning: Test data not found. Make sure init-db.sql has been run."
60
+ end
61
+ rescue PG::Error => e
62
+ warn "Error checking test data: #{e.message}"
63
+ end
64
+ end
65
+
66
+ def cleanup_test_data
67
+ # In Docker environment, we don't need to cleanup as containers are disposable
68
+ # In local environment, you might want to clean up test data here
69
+ # For now, we'll leave the data intact for debugging purposes
70
+ end
71
+ end
data.tar.gz.sig CHANGED
Binary file