database-model-generator 0.6.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,287 @@
1
+ require_relative '../../database_model_generator'
2
+
3
+ begin
4
+ require 'oci8'
5
+ rescue LoadError
6
+ # OCI8 not available - Oracle support will be disabled
7
+ end
8
+
9
+ module Oracle
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?(OCI8) && connection.is_a?(OCI8)
18
+ raise ArgumentError, "Connection must be an OCI8 object. Install 'oci8' gem for Oracle support."
19
+ end
20
+ end
21
+
22
+ def normalize_table_name(table)
23
+ table.upcase
24
+ end
25
+
26
+ def check_table_exists(table)
27
+ cursor = nil
28
+ begin
29
+ sql = "SELECT COUNT(*) FROM USER_TABLES WHERE TABLE_NAME = ?"
30
+ cursor = @connection.parse(sql)
31
+ cursor.bind_param(1, table.upcase)
32
+ cursor.exec
33
+ result = cursor.fetch
34
+ result && result[0] > 0
35
+ rescue => e
36
+ puts "Error checking table existence: #{e.message}"
37
+ false
38
+ ensure
39
+ cursor.close if cursor
40
+ end
41
+ end
42
+
43
+ <<<<<<< Updated upstream
44
+ # Public method to get readable constraint information
45
+ def constraint_summary
46
+ return {} unless generated?
47
+
48
+ summary = Hash.new { |h, k| h[k] = [] }
49
+ @constraints.each do |constraint|
50
+ type = case constraint['CONSTRAINT_TYPE']
51
+ when 'P' then 'Primary Key'
52
+ when 'R' then 'Foreign Key'
53
+ when 'U' then 'Unique'
54
+ when 'C' then 'Check'
55
+ else constraint['CONSTRAINT_TYPE']
56
+ end
57
+ summary[constraint['COLUMN_NAME'].downcase] << type
58
+ end
59
+ summary
60
+ end
61
+
62
+ private
63
+
64
+ # Reset internal state - useful for error recovery
65
+ def reset_state
66
+ @constraints.clear
67
+ @primary_keys.clear
68
+ @foreign_keys.clear
69
+ @dependencies.clear
70
+ @belongs_to.clear
71
+ @column_info.clear
72
+ @table = nil
73
+ @model = nil
74
+ @view = nil
75
+ end
76
+
77
+ # Generate a more intelligent model name from table name
78
+ def generate_model_name(table)
79
+ # Handle common table naming patterns
80
+ name = table.split('_').map{ |part| part.downcase.capitalize }.join
81
+
82
+ # Remove trailing 's' for pluralized table names
83
+ name.chop! if name.length > 1 && name[-1].chr.upcase == 'S'
84
+
85
+ # Handle special cases
86
+ case name.downcase
87
+ when 'people'
88
+ 'Person'
89
+ when 'children'
90
+ 'Child'
91
+ when 'data'
92
+ 'Datum'
93
+ else
94
+ name
95
+ end
96
+ end
97
+
98
+ =======
99
+ >>>>>>> Stashed changes
100
+ def get_column_info
101
+ cursor = nil
102
+ begin
103
+ if @view
104
+ sql = "SELECT COLUMN_NAME, DATA_TYPE, DATA_LENGTH, DATA_PRECISION, DATA_SCALE, NULLABLE
105
+ FROM USER_TAB_COLUMNS WHERE TABLE_NAME = ? ORDER BY COLUMN_ID"
106
+ else
107
+ sql = "SELECT COLUMN_NAME, DATA_TYPE, DATA_LENGTH, DATA_PRECISION, DATA_SCALE, NULLABLE
108
+ FROM USER_TAB_COLUMNS WHERE TABLE_NAME = ? ORDER BY COLUMN_ID"
109
+ end
110
+
111
+ cursor = @connection.parse(sql)
112
+ cursor.bind_param(1, @table)
113
+ cursor.exec
114
+
115
+ @column_info = []
116
+ while row = cursor.fetch_hash
117
+ @column_info << OracleColumnInfo.new(row)
118
+ end
119
+ rescue => e
120
+ raise "Error retrieving column information: #{e.message}"
121
+ ensure
122
+ cursor.close if cursor
123
+ end
124
+ end
125
+
126
+ def get_primary_keys
127
+ cursor = nil
128
+ begin
129
+ sql = "SELECT column_name FROM user_cons_columns
130
+ WHERE constraint_name = (
131
+ SELECT constraint_name FROM user_constraints
132
+ WHERE table_name = ? AND constraint_type = 'P'
133
+ ) ORDER BY position"
134
+
135
+ cursor = @connection.parse(sql)
136
+ cursor.bind_param(1, @table)
137
+ cursor.exec
138
+
139
+ @primary_keys = []
140
+ while row = cursor.fetch
141
+ @primary_keys << row[0]
142
+ end
143
+ rescue => e
144
+ raise "Error retrieving primary keys: #{e.message}"
145
+ ensure
146
+ cursor.close if cursor
147
+ end
148
+ end
149
+
150
+ def get_foreign_keys
151
+ cursor = nil
152
+ begin
153
+ sql = "SELECT column_name FROM user_cons_columns
154
+ WHERE constraint_name IN (
155
+ SELECT constraint_name FROM user_constraints
156
+ WHERE table_name = ? AND constraint_type = 'R'
157
+ )"
158
+
159
+ cursor = @connection.parse(sql)
160
+ cursor.bind_param(1, @table)
161
+ cursor.exec
162
+
163
+ @foreign_keys = []
164
+ while row = cursor.fetch
165
+ @foreign_keys << row[0]
166
+ end
167
+ rescue => e
168
+ raise "Error retrieving foreign keys: #{e.message}"
169
+ ensure
170
+ cursor.close if cursor
171
+ end
172
+ end
173
+
174
+ def get_constraints
175
+ cursor = nil
176
+ begin
177
+ sql = "SELECT c.constraint_name, c.constraint_type, cc.column_name, c.search_condition
178
+ FROM user_constraints c, user_cons_columns cc
179
+ WHERE c.table_name = ? AND c.constraint_name = cc.constraint_name"
180
+
181
+ cursor = @connection.parse(sql)
182
+ cursor.bind_param(1, @table)
183
+ cursor.exec
184
+
185
+ @constraints = []
186
+ while row = cursor.fetch_hash
187
+ @constraints << row
188
+ end
189
+ rescue => e
190
+ raise "Error retrieving constraints: #{e.message}"
191
+ ensure
192
+ cursor.close if cursor
193
+ end
194
+ end
195
+
196
+ def get_dependencies
197
+ cursor = nil
198
+ begin
199
+ sql = "SELECT REFERENCED_NAME as NAME, REFERENCED_TYPE as TYPE
200
+ FROM USER_DEPENDENCIES WHERE NAME = ? AND TYPE = 'TABLE'"
201
+
202
+ cursor = @connection.parse(sql)
203
+ cursor.bind_param(1, @table)
204
+ cursor.exec
205
+
206
+ @dependencies = []
207
+ while row = cursor.fetch_hash
208
+ @dependencies << row
209
+ end
210
+ rescue => e
211
+ raise "Error retrieving dependencies: #{e.message}"
212
+ ensure
213
+ cursor.close if cursor
214
+ end
215
+ end
216
+
217
+ def format_constraint_type(constraint)
218
+ case constraint['CONSTRAINT_TYPE']
219
+ when 'P' then 'Primary Key'
220
+ when 'R' then 'Foreign Key'
221
+ when 'U' then 'Unique'
222
+ when 'C' then 'Check'
223
+ else constraint['CONSTRAINT_TYPE']
224
+ end
225
+ end
226
+
227
+ def get_constraint_column_name(constraint)
228
+ constraint['COLUMN_NAME']
229
+ end
230
+
231
+ def get_constraint_text(constraint)
232
+ constraint['SEARCH_CONDITION']
233
+ end
234
+
235
+ def is_string_type?(column)
236
+ column.data_type.to_s.downcase =~ /(char|varchar|varchar2|clob)/
237
+ end
238
+
239
+ def is_date_type?(column)
240
+ column.data_type.to_s.downcase =~ /(date|timestamp)/
241
+ end
242
+
243
+ def is_text_type?(column)
244
+ column.data_type.to_s.downcase =~ /(varchar|char|clob)/
245
+ end
246
+
247
+ def get_column_size(column)
248
+ column.data_size
249
+ end
250
+
251
+ def build_full_text_index_sql(column)
252
+ "CREATE INDEX idx_#{@table.downcase}_#{column.name.downcase}_text ON #{@table.upcase} (#{column.name.upcase}) INDEXTYPE IS CTXSYS.CONTEXT"
253
+ end
254
+
255
+ def get_full_text_index_type
256
+ "Oracle Text Index"
257
+ end
258
+
259
+ def find_fk_table(fk)
260
+ # Oracle-specific logic for finding referenced table
261
+ fk.gsub(/_id$/i, '').pluralize rescue "#{fk.gsub(/_id$/i, '')}s"
262
+ end
263
+
264
+ def disconnect
265
+ @connection.logoff if @connection
266
+ end
267
+ end
268
+
269
+ # Oracle column info wrapper
270
+ class OracleColumnInfo
271
+ attr_reader :name, :data_type, :data_size, :precision, :scale, :nullable
272
+
273
+ def initialize(row)
274
+ @name = row['COLUMN_NAME']
275
+ @data_type = row['DATA_TYPE']
276
+ @data_size = row['DATA_LENGTH']
277
+ @precision = row['DATA_PRECISION']
278
+ @scale = row['DATA_SCALE']
279
+ @nullable = row['NULLABLE'] == 'Y'
280
+ end
281
+
282
+ def nullable?
283
+ @nullable
284
+ end
285
+ end
286
+ end
287
+ end
@@ -0,0 +1,281 @@
1
+ require_relative '../../database_model_generator'
2
+
3
+ begin
4
+ require 'tiny_tds'
5
+ rescue LoadError
6
+ # TinyTds not available - SQL Server support will be disabled
7
+ end
8
+
9
+ module SqlServer
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?(TinyTds) && connection.is_a?(TinyTds::Client)
18
+ raise ArgumentError, "Connection must be a TinyTds::Client object. Install 'tiny_tds' gem for SQL Server support."
19
+ end
20
+ end
21
+
22
+ def normalize_table_name(table)
23
+ table # SQL Server table names are case-sensitive, preserve as-is
24
+ end
25
+
26
+ def check_table_exists(table)
27
+ begin
28
+ sql = "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '#{table}'"
29
+ result = @connection.execute(sql)
30
+ row = result.first
31
+ row && row.values.first > 0
32
+ rescue => e
33
+ puts "Error checking table existence: #{e.message}"
34
+ false
35
+ ensure
36
+ result.cancel if result && result.respond_to?(:cancel)
37
+ end
38
+ end
39
+
40
+ def get_column_info
41
+ begin
42
+ sql = <<~SQL
43
+ SELECT
44
+ COLUMN_NAME,
45
+ DATA_TYPE,
46
+ CHARACTER_MAXIMUM_LENGTH,
47
+ NUMERIC_PRECISION,
48
+ NUMERIC_SCALE,
49
+ IS_NULLABLE,
50
+ COLUMN_DEFAULT
51
+ FROM INFORMATION_SCHEMA.COLUMNS
52
+ WHERE TABLE_NAME = '#{@table}'
53
+ ORDER BY ORDINAL_POSITION
54
+ SQL
55
+
56
+ result = @connection.execute(sql)
57
+ @column_info = []
58
+
59
+ result.each do |row|
60
+ @column_info << SqlServerColumnInfo.new(row)
61
+ end
62
+ rescue => e
63
+ raise "Error retrieving column information: #{e.message}"
64
+ ensure
65
+ result.cancel if result && result.respond_to?(:cancel)
66
+ end
67
+ end
68
+
69
+ def get_primary_keys
70
+ begin
71
+ sql = <<~SQL
72
+ SELECT c.COLUMN_NAME
73
+ FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
74
+ JOIN INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE ccu ON tc.CONSTRAINT_NAME = ccu.CONSTRAINT_NAME
75
+ JOIN INFORMATION_SCHEMA.COLUMNS c ON ccu.COLUMN_NAME = c.COLUMN_NAME AND ccu.TABLE_NAME = c.TABLE_NAME
76
+ WHERE tc.TABLE_NAME = '#{@table}' AND tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
77
+ ORDER BY c.ORDINAL_POSITION
78
+ SQL
79
+
80
+ result = @connection.execute(sql)
81
+ @primary_keys = []
82
+
83
+ result.each do |row|
84
+ @primary_keys << row['COLUMN_NAME']
85
+ end
86
+ rescue => e
87
+ raise "Error retrieving primary keys: #{e.message}"
88
+ ensure
89
+ result.cancel if result && result.respond_to?(:cancel)
90
+ end
91
+ end
92
+
93
+ def get_foreign_keys
94
+ begin
95
+ sql = <<~SQL
96
+ SELECT ccu.COLUMN_NAME
97
+ FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
98
+ JOIN INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE ccu ON tc.CONSTRAINT_NAME = ccu.CONSTRAINT_NAME
99
+ WHERE tc.TABLE_NAME = '#{@table}' AND tc.CONSTRAINT_TYPE = 'FOREIGN KEY'
100
+ SQL
101
+
102
+ result = @connection.execute(sql)
103
+ @foreign_keys = []
104
+
105
+ result.each do |row|
106
+ @foreign_keys << row['COLUMN_NAME']
107
+ end
108
+ rescue => e
109
+ raise "Error retrieving foreign keys: #{e.message}"
110
+ ensure
111
+ result.cancel if result && result.respond_to?(:cancel)
112
+ end
113
+ end
114
+
115
+ def get_constraints
116
+ begin
117
+ sql = <<~SQL
118
+ SELECT
119
+ tc.CONSTRAINT_NAME,
120
+ tc.CONSTRAINT_TYPE,
121
+ ccu.COLUMN_NAME,
122
+ cc.CHECK_CLAUSE
123
+ FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
124
+ LEFT JOIN INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE ccu ON tc.CONSTRAINT_NAME = ccu.CONSTRAINT_NAME
125
+ LEFT JOIN INFORMATION_SCHEMA.CHECK_CONSTRAINTS cc ON tc.CONSTRAINT_NAME = cc.CONSTRAINT_NAME
126
+ WHERE tc.TABLE_NAME = '#{@table}'
127
+ SQL
128
+
129
+ result = @connection.execute(sql)
130
+ @constraints = []
131
+
132
+ result.each do |row|
133
+ @constraints << {
134
+ 'CONSTRAINT_NAME' => row['CONSTRAINT_NAME'],
135
+ 'CONSTRAINT_TYPE' => row['CONSTRAINT_TYPE'],
136
+ 'COLUMN_NAME' => row['COLUMN_NAME'],
137
+ 'CHECK_CLAUSE' => row['CHECK_CLAUSE']
138
+ }
139
+ end
140
+ rescue => e
141
+ raise "Error retrieving constraints: #{e.message}"
142
+ ensure
143
+ result.cancel if result && result.respond_to?(:cancel)
144
+ end
145
+ end
146
+
147
+ def get_dependencies
148
+ begin
149
+ sql = <<~SQL
150
+ SELECT
151
+ OBJECT_NAME(referencing_id) AS NAME,
152
+ 'TABLE' AS TYPE
153
+ FROM sys.sql_expression_dependencies sed
154
+ JOIN sys.objects o ON sed.referenced_id = o.object_id
155
+ WHERE o.name = '#{@table}' AND o.type = 'U'
156
+ SQL
157
+
158
+ result = @connection.execute(sql)
159
+ @dependencies = []
160
+
161
+ result.each do |row|
162
+ @dependencies << {
163
+ 'NAME' => row['NAME'],
164
+ 'TYPE' => row['TYPE']
165
+ }
166
+ end
167
+ rescue => e
168
+ # Dependencies query may fail on some SQL Server versions, continue without error
169
+ @dependencies = []
170
+ ensure
171
+ result.cancel if result && result.respond_to?(:cancel)
172
+ end
173
+ end
174
+
175
+ def format_constraint_type(constraint)
176
+ case constraint['CONSTRAINT_TYPE']
177
+ when 'PRIMARY KEY' then 'Primary Key'
178
+ when 'FOREIGN KEY' then 'Foreign Key'
179
+ when 'UNIQUE' then 'Unique'
180
+ when 'CHECK' then 'Check'
181
+ else constraint['CONSTRAINT_TYPE']
182
+ end
183
+ end
184
+
185
+ def get_constraint_column_name(constraint)
186
+ constraint['COLUMN_NAME']
187
+ end
188
+
189
+ def get_constraint_text(constraint)
190
+ constraint['CHECK_CLAUSE']
191
+ end
192
+
193
+ def is_string_type?(column)
194
+ column.data_type.downcase =~ /(char|varchar|nchar|nvarchar|text|ntext)/
195
+ end
196
+
197
+ def is_date_type?(column)
198
+ column.data_type.downcase =~ /(date|time|datetime|datetime2|smalldatetime|datetimeoffset)/
199
+ end
200
+
201
+ def is_text_type?(column)
202
+ column.data_type.downcase =~ /(varchar|nvarchar|char|nchar|text|ntext)/
203
+ end
204
+
205
+ def get_column_size(column)
206
+ column.character_maximum_length
207
+ end
208
+
209
+ def build_full_text_index_sql(column)
210
+ "CREATE FULLTEXT INDEX ON #{@table} (#{column.name.upcase}) KEY INDEX PK_#{@table}"
211
+ end
212
+
213
+ def get_full_text_index_type
214
+ "SQL Server Full-Text Index"
215
+ end
216
+
217
+ def find_fk_table(fk)
218
+ # SQL Server-specific logic for finding referenced table
219
+ # Try to get actual referenced table from foreign key constraints
220
+ begin
221
+ sql = <<~SQL
222
+ SELECT
223
+ OBJECT_NAME(f.referenced_object_id) AS referenced_table
224
+ FROM sys.foreign_keys f
225
+ JOIN sys.foreign_key_columns fc ON f.object_id = fc.constraint_object_id
226
+ JOIN sys.columns c ON fc.parent_object_id = c.object_id AND fc.parent_column_id = c.column_id
227
+ WHERE f.parent_object_id = OBJECT_ID('#{@table}') AND c.name = '#{fk}'
228
+ SQL
229
+
230
+ result = @connection.execute(sql)
231
+ row = result.first
232
+ if row && row['referenced_table']
233
+ return row['referenced_table'].downcase
234
+ end
235
+ rescue => e
236
+ # Fall back to naming convention if query fails
237
+ ensure
238
+ result.cancel if result && result.respond_to?(:cancel)
239
+ end
240
+
241
+ # Fallback to naming convention
242
+ fk.gsub(/_id$/i, '').pluralize rescue "#{fk.gsub(/_id$/i, '')}s"
243
+ end
244
+
245
+ def disconnect
246
+ @connection.close if @connection
247
+ end
248
+ end
249
+
250
+ # SQL Server column info wrapper
251
+ class SqlServerColumnInfo
252
+ attr_reader :name, :data_type, :character_maximum_length, :numeric_precision, :numeric_scale, :nullable, :column_default
253
+
254
+ def initialize(row)
255
+ @name = row['COLUMN_NAME']
256
+ @data_type = row['DATA_TYPE']
257
+ @character_maximum_length = row['CHARACTER_MAXIMUM_LENGTH']
258
+ @numeric_precision = row['NUMERIC_PRECISION']
259
+ @numeric_scale = row['NUMERIC_SCALE']
260
+ @nullable = row['IS_NULLABLE'] == 'YES'
261
+ @column_default = row['COLUMN_DEFAULT']
262
+ end
263
+
264
+ def nullable?
265
+ @nullable
266
+ end
267
+
268
+ def data_size
269
+ @character_maximum_length
270
+ end
271
+
272
+ def precision
273
+ @numeric_precision
274
+ end
275
+
276
+ def scale
277
+ @numeric_scale
278
+ end
279
+ end
280
+ end
281
+ end