jackcess-rb 0.1.0-java

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,440 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jackcess
4
+ # Represents a Microsoft Access database file (.mdb or .accdb).
5
+ #
6
+ # This class provides the main entry point for interacting with Access databases
7
+ # through the Jackcess library. It supports opening existing databases, creating
8
+ # new ones, and managing tables within the database.
9
+ #
10
+ # @example Open an existing database
11
+ # db = Jackcess::Database.open('path/to/database.accdb')
12
+ # db.table_names
13
+ # # => ["Customers", "Orders", "Products"]
14
+ # db.close
15
+ #
16
+ # @example Create a new database with block
17
+ # Jackcess::Database.create('new.accdb', :v2007) do |db|
18
+ # db.create_table('Users') do |t|
19
+ # t.column 'ID', :long, auto_number: true
20
+ # t.column 'Name', :text, length: 255
21
+ # t.primary_key 'ID'
22
+ # end
23
+ # end # Database automatically closed
24
+ class Database
25
+ # The underlying Java database object from Jackcess
26
+ # @return [Java::ComHealthmarketscienceJackcess::Database]
27
+ attr_reader :java_database
28
+
29
+ # Creates a new Database instance wrapping a Java database object.
30
+ # This method is typically not called directly; use {.open} or {.create} instead.
31
+ #
32
+ # @param java_database [Java::ComHealthmarketscienceJackcess::Database] The Java database object
33
+ # @api private
34
+ def initialize(java_database)
35
+ @java_database = java_database
36
+ @closed = false
37
+ end
38
+
39
+ # Opens an existing Access database file.
40
+ #
41
+ # This method opens an existing Microsoft Access database file (.mdb or .accdb)
42
+ # for reading and/or writing. If a block is given, the database will be
43
+ # automatically closed when the block exits, even if an exception is raised.
44
+ #
45
+ # @param path [String] The file path to the database
46
+ # @param options [Hash] Optional configuration
47
+ # @option options [Boolean] :readonly (false) Open the database in read-only mode
48
+ # @option options [Boolean] :auto_sync (true) Automatically sync changes to disk
49
+ #
50
+ # @return [Database] A new Database instance (without block)
51
+ # @yield [db] Passes the database to the block
52
+ # @yieldparam db [Database] The opened database
53
+ # @yieldreturn [Object] The return value of the block (with block form)
54
+ #
55
+ # @raise [ArgumentError] If path is nil, empty, or not a String
56
+ # @raise [DatabaseError] If the file doesn't exist, is a directory, or cannot be opened
57
+ #
58
+ # @example Open database without block
59
+ # db = Jackcess::Database.open('data/northwind.accdb')
60
+ # # ... work with database ...
61
+ # db.close
62
+ #
63
+ # @example Open database with block (auto-close)
64
+ # Jackcess::Database.open('data/northwind.accdb') do |db|
65
+ # puts db.table_names
66
+ # end # Database automatically closed
67
+ #
68
+ # @example Open in read-only mode
69
+ # db = Jackcess::Database.open('data/northwind.accdb', readonly: true)
70
+ def self.open(path, options = {})
71
+ raise ArgumentError, "Database path cannot be nil" if path.nil?
72
+ raise ArgumentError, "Database path must be a String" unless path.is_a?(String)
73
+ raise ArgumentError, "Database path cannot be empty" if path.empty?
74
+
75
+ path = File.expand_path(path)
76
+ raise DatabaseError, "Database file not found: #{path}" unless File.exist?(path)
77
+ raise DatabaseError, "Path is a directory, not a file: #{path}" if File.directory?(path)
78
+
79
+ begin
80
+ builder = DatabaseBuilder.new(java.io.File.new(path))
81
+ builder.set_read_only(options[:readonly]) if options.key?(:readonly)
82
+ builder.set_auto_sync(options[:auto_sync]) if options.key?(:auto_sync)
83
+
84
+ java_db = builder.open
85
+ db = new(java_db)
86
+
87
+ if block_given?
88
+ begin
89
+ yield db
90
+ ensure
91
+ db.close
92
+ end
93
+ else
94
+ db
95
+ end
96
+ rescue Java::JavaIo::IOException => e
97
+ raise DatabaseError, "Failed to open database '#{path}': #{e.message}"
98
+ rescue Java::JavaLang::Exception => e
99
+ raise DatabaseError, "Failed to open database '#{path}': #{e.message}"
100
+ end
101
+ end
102
+
103
+ # Creates a new Access database file.
104
+ #
105
+ # This method creates a new Microsoft Access database file in the specified format.
106
+ # If a block is given, the database will be automatically closed when the block exits.
107
+ #
108
+ # @param path [String] The file path for the new database
109
+ # @param format [Symbol] The Access file format (:v2000, :v2003, :v2007, or :v2010)
110
+ #
111
+ # @return [Database] A new Database instance (without block)
112
+ # @yield [db] Passes the database to the block
113
+ # @yieldparam db [Database] The created database
114
+ # @yieldreturn [Object] The return value of the block (with block form)
115
+ #
116
+ # @raise [ArgumentError] If path is nil, empty, not a String, or format is invalid
117
+ # @raise [DatabaseError] If the database cannot be created
118
+ #
119
+ # @example Create a database without block
120
+ # db = Jackcess::Database.create('new.accdb', :v2007)
121
+ # # ... work with database ...
122
+ # db.close
123
+ #
124
+ # @example Create a database with block (auto-close)
125
+ # Jackcess::Database.create('new.accdb', :v2007) do |db|
126
+ # db.create_table('Products') do |t|
127
+ # t.column 'ID', :long, auto_number: true
128
+ # t.column 'Name', :text, length: 255
129
+ # t.primary_key 'ID'
130
+ # end
131
+ # end # Database automatically closed
132
+ def self.create(path, format = :v2000)
133
+ raise ArgumentError, "Database path cannot be nil" if path.nil?
134
+ raise ArgumentError, "Database path must be a String" unless path.is_a?(String)
135
+ raise ArgumentError, "Database path cannot be empty" if path.empty?
136
+ raise ArgumentError, "Database format cannot be nil" if format.nil?
137
+
138
+ path = File.expand_path(path)
139
+
140
+ begin
141
+ file_format = case format
142
+ when :v2000
143
+ com.healthmarketscience.jackcess.Database::FileFormat::V2000
144
+ when :v2003
145
+ com.healthmarketscience.jackcess.Database::FileFormat::V2003
146
+ when :v2007
147
+ com.healthmarketscience.jackcess.Database::FileFormat::V2007
148
+ when :v2010
149
+ com.healthmarketscience.jackcess.Database::FileFormat::V2010
150
+ else
151
+ raise DatabaseError, "Unknown database format: #{format}. Valid formats are: :v2000, :v2003, :v2007, :v2010"
152
+ end
153
+
154
+ java_db = DatabaseBuilder.create(file_format, java.io.File.new(path))
155
+ db = new(java_db)
156
+
157
+ if block_given?
158
+ begin
159
+ yield db
160
+ ensure
161
+ db.close
162
+ end
163
+ else
164
+ db
165
+ end
166
+ rescue Java::JavaIo::IOException => e
167
+ raise DatabaseError, "Failed to create database '#{path}': #{e.message}"
168
+ rescue Java::JavaLang::Exception => e
169
+ raise DatabaseError, "Failed to create database '#{path}': #{e.message}"
170
+ end
171
+ end
172
+
173
+ # Retrieves a table by name from the database.
174
+ #
175
+ # @param name [String, Symbol] The name of the table to retrieve
176
+ #
177
+ # @return [Table] The requested table
178
+ #
179
+ # @raise [ArgumentError] If name is nil or not a String/Symbol
180
+ # @raise [TableNotFoundError] If the table doesn't exist
181
+ # @raise [DatabaseError] If the database is closed or table cannot be accessed
182
+ #
183
+ # @example Get a table
184
+ # table = db.table('Customers')
185
+ # puts table.name # => "Customers"
186
+ def table(name)
187
+ check_closed!
188
+ raise ArgumentError, "Table name cannot be nil" if name.nil?
189
+ raise ArgumentError, "Table name must be a String or Symbol" unless name.is_a?(String) || name.is_a?(Symbol)
190
+
191
+ name = name.to_s
192
+ begin
193
+ java_table = @java_database.get_table(name)
194
+ raise TableNotFoundError, "Table not found: #{name}" if java_table.nil?
195
+
196
+ Table.new(java_table, self)
197
+ rescue Java::JavaIo::IOException => e
198
+ raise DatabaseError, "Failed to access table '#{name}': #{e.message}"
199
+ end
200
+ end
201
+
202
+ # Returns a sorted array of all table names in the database.
203
+ #
204
+ # @return [Array<String>] Sorted array of table names
205
+ #
206
+ # @raise [DatabaseError] If the database is closed or table names cannot be retrieved
207
+ #
208
+ # @example List all tables
209
+ # db.table_names
210
+ # # => ["Customers", "Orders", "Products"]
211
+ def table_names
212
+ check_closed!
213
+
214
+ begin
215
+ @java_database.get_table_names.to_a.sort
216
+ rescue Java::JavaIo::IOException => e
217
+ raise DatabaseError, "Failed to get table names: #{e.message}"
218
+ end
219
+ end
220
+
221
+ # Creates a new table in the database.
222
+ #
223
+ # This method uses a DSL (Domain-Specific Language) for defining the table schema.
224
+ # The block receives a table builder object that supports `column` and `primary_key` methods.
225
+ #
226
+ # @param name [String, Symbol] The name for the new table
227
+ #
228
+ # @return [Table] The newly created table
229
+ #
230
+ # @yield [t] Passes a table builder to the block for defining columns
231
+ # @yieldparam t [TableBuilderDSL] The table builder
232
+ #
233
+ # @raise [ArgumentError] If name is nil, empty, or not a String/Symbol
234
+ # @raise [DatabaseError] If the database is closed or table cannot be created
235
+ #
236
+ # @example Create a simple table
237
+ # table = db.create_table('Users') do |t|
238
+ # t.column 'ID', :long, auto_number: true
239
+ # t.column 'Name', :text, length: 255
240
+ # t.column 'Email', :text, length: 255
241
+ # t.column 'CreatedAt', :date_time
242
+ # t.primary_key 'ID'
243
+ # end
244
+ #
245
+ # @see TableBuilderDSL
246
+ def create_table(name, &block)
247
+ check_closed!
248
+ raise ArgumentError, "Table name cannot be nil" if name.nil?
249
+ raise ArgumentError, "Table name must be a String or Symbol" unless name.is_a?(String) || name.is_a?(Symbol)
250
+ raise ArgumentError, "Table name cannot be empty" if name.to_s.empty?
251
+
252
+ begin
253
+ builder = TableBuilder.new(name)
254
+ table_builder = TableBuilderDSL.new(builder)
255
+ table_builder.instance_eval(&block) if block_given?
256
+
257
+ java_table = builder.to_table(@java_database)
258
+ Table.new(java_table, self)
259
+ rescue Java::JavaIo::IOException => e
260
+ raise DatabaseError, "Failed to create table '#{name}': #{e.message}"
261
+ rescue Java::JavaLang::Exception => e
262
+ raise DatabaseError, "Failed to create table '#{name}': #{e.message}"
263
+ end
264
+ end
265
+
266
+ # Executes a block within a transaction context.
267
+ #
268
+ # Note: Jackcess doesn't have explicit ACID transaction support like traditional
269
+ # databases. This method provides a semantic grouping for operations and will
270
+ # wrap exceptions in DatabaseError. For true atomicity, consider implementing
271
+ # checkpointing or using database-level features if available.
272
+ #
273
+ # @yield The block of operations to execute
274
+ #
275
+ # @raise [DatabaseError] If the database is closed or if an error occurs during the transaction
276
+ #
277
+ # @example Execute a transaction
278
+ # db.transaction do
279
+ # table = db.table('Accounts')
280
+ # # Perform multiple operations
281
+ # end
282
+ def transaction
283
+ check_closed!
284
+
285
+ # Note: Jackcess doesn't have explicit transaction support like traditional databases
286
+ # This is a placeholder for future enhancement
287
+ yield
288
+ rescue StandardError => e
289
+ raise DatabaseError, "Transaction failed: #{e.message}"
290
+ end
291
+
292
+ # Closes the database and flushes any pending changes to disk.
293
+ #
294
+ # After calling this method, the database cannot be used. Calling close on an
295
+ # already-closed database is safe and will not raise an error.
296
+ #
297
+ # @return [nil]
298
+ #
299
+ # @raise [DatabaseError] If an error occurs during the close operation
300
+ #
301
+ # @example Close a database
302
+ # db = Jackcess::Database.open('data.accdb')
303
+ # # ... work with database ...
304
+ # db.close
305
+ def close
306
+ return if @closed
307
+
308
+ begin
309
+ @java_database.close
310
+ @closed = true
311
+ rescue Java::JavaIo::IOException => e
312
+ # Log the error but mark as closed to prevent further operations
313
+ @closed = true
314
+ raise DatabaseError, "Failed to close database cleanly: #{e.message}"
315
+ end
316
+ end
317
+
318
+ # Checks whether the database has been closed.
319
+ #
320
+ # @return [Boolean] true if the database is closed, false otherwise
321
+ #
322
+ # @example Check if database is closed
323
+ # db = Jackcess::Database.open('data.accdb')
324
+ # db.closed? # => false
325
+ # db.close
326
+ # db.closed? # => true
327
+ def closed?
328
+ @closed
329
+ end
330
+
331
+ # Flushes any pending changes to disk.
332
+ #
333
+ # This method forces all buffered changes to be written to the database file.
334
+ # It's generally not necessary to call this manually unless you need to ensure
335
+ # changes are persisted immediately.
336
+ #
337
+ # @return [nil]
338
+ #
339
+ # @raise [DatabaseError] If the database is closed or flush fails
340
+ #
341
+ # @example Flush changes
342
+ # db = Jackcess::Database.open('data.accdb')
343
+ # # ... make changes ...
344
+ # db.flush # Ensure changes are written to disk
345
+ def flush
346
+ check_closed!
347
+
348
+ begin
349
+ @java_database.flush
350
+ rescue Java::JavaIo::IOException => e
351
+ raise DatabaseError, "Failed to flush database: #{e.message}"
352
+ end
353
+ end
354
+
355
+ private
356
+
357
+ def check_closed!
358
+ raise DatabaseError, "Database is closed" if @closed
359
+ end
360
+
361
+ # DSL (Domain-Specific Language) for building table schemas.
362
+ #
363
+ # This class provides a fluent interface for defining table columns and indexes
364
+ # when creating new tables. It's used internally by {Database#create_table}.
365
+ #
366
+ # @api private
367
+ class TableBuilderDSL
368
+ # Creates a new table builder DSL instance.
369
+ #
370
+ # @param builder [Java::ComHealthmarketscienceJackcess::TableBuilder] The underlying Java table builder
371
+ # @api private
372
+ def initialize(builder)
373
+ @builder = builder
374
+ @primary_key_column = nil
375
+ end
376
+
377
+ # Defines a column in the table.
378
+ #
379
+ # @param name [String, Symbol] The column name
380
+ # @param type [Symbol] The column data type (:text, :memo, :byte, :int, :long, :float, :double, :currency, :date_time, :boolean, :binary, :guid)
381
+ # @param options [Hash] Column options
382
+ # @option options [Integer] :length The maximum length for text columns
383
+ # @option options [Boolean] :auto_number (false) Whether this is an auto-incrementing column
384
+ #
385
+ # @return [void]
386
+ #
387
+ # @raise [ArgumentError] If name or type is invalid
388
+ #
389
+ # @example Define columns
390
+ # db.create_table('Users') do |t|
391
+ # t.column 'ID', :long, auto_number: true
392
+ # t.column 'Name', :text, length: 255
393
+ # t.column 'Email', :text, length: 255
394
+ # t.column 'Age', :int
395
+ # t.column 'Balance', :currency
396
+ # t.column 'IsActive', :boolean
397
+ # t.column 'CreatedAt', :date_time
398
+ # end
399
+ def column(name, type, options = {})
400
+ raise ArgumentError, "Column name cannot be nil" if name.nil?
401
+ raise ArgumentError, "Column name must be a String or Symbol" unless name.is_a?(String) || name.is_a?(Symbol)
402
+ raise ArgumentError, "Column name cannot be empty" if name.to_s.empty?
403
+ raise ArgumentError, "Column type cannot be nil" if type.nil?
404
+
405
+ data_type = TypeConverter.ruby_type_to_data_type(type)
406
+ col_builder = ColumnBuilder.new(name, data_type)
407
+
408
+ col_builder.set_length(options[:length]) if options[:length]
409
+ col_builder.set_auto_number(options[:auto_number]) if options[:auto_number]
410
+
411
+ @builder.add_column(col_builder.to_column)
412
+ end
413
+
414
+ # Defines the primary key for the table.
415
+ #
416
+ # @param column_name [String, Symbol] The name of the column to use as primary key
417
+ #
418
+ # @return [void]
419
+ #
420
+ # @raise [ArgumentError] If column_name is nil
421
+ #
422
+ # @example Set primary key
423
+ # db.create_table('Users') do |t|
424
+ # t.column 'ID', :long, auto_number: true
425
+ # t.column 'Name', :text, length: 255
426
+ # t.primary_key 'ID'
427
+ # end
428
+ def primary_key(column_name)
429
+ raise ArgumentError, "Primary key column name cannot be nil" if column_name.nil?
430
+
431
+ index_builder = com.healthmarketscience.jackcess.IndexBuilder.new(
432
+ com.healthmarketscience.jackcess.IndexBuilder::PRIMARY_KEY_NAME
433
+ )
434
+ index_builder.add_columns(column_name)
435
+ index_builder.set_primary_key
436
+ @builder.add_index(index_builder)
437
+ end
438
+ end
439
+ end
440
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jackcess
4
+ class Error < StandardError; end
5
+ class DatabaseError < Error; end
6
+ class TableNotFoundError < Error; end
7
+ class ColumnNotFoundError < Error; end
8
+ class InvalidTypeError < Error; end
9
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jackcess
4
+ # Represents an index on a table in an Access database.
5
+ #
6
+ # Indexes provide fast lookups and enforce uniqueness constraints.
7
+ # This class wraps Jackcess's Index object to provide a Ruby-friendly interface.
8
+ #
9
+ # @example Inspect table indexes
10
+ # table = db.table('Users')
11
+ # table.indexes.each do |index|
12
+ # puts "#{index.name}: #{index.columns.join(', ')}"
13
+ # puts "Primary key: #{index.primary_key?}"
14
+ # puts "Unique: #{index.unique?}"
15
+ # end
16
+ class Index
17
+ # The underlying Java index object from Jackcess
18
+ # @return [Java::ComHealthmarketscienceJackcess::Index]
19
+ attr_reader :java_index
20
+
21
+ # Creates a new Index instance wrapping a Java index object.
22
+ #
23
+ # @param java_index [Java::ComHealthmarketscienceJackcess::Index] The Java index object
24
+ # @api private
25
+ def initialize(java_index)
26
+ @java_index = java_index
27
+ end
28
+
29
+ # Returns the name of the index.
30
+ #
31
+ # @return [String] The index name
32
+ #
33
+ # @example Get index name
34
+ # index.name # => "PrimaryKey"
35
+ def name
36
+ @java_index.get_name
37
+ end
38
+
39
+ # Returns an array of column names that comprise this index.
40
+ #
41
+ # @return [Array<String>] Array of column names in the index
42
+ #
43
+ # @example Get index columns
44
+ # index.columns # => ["UserID", "Email"]
45
+ def columns
46
+ @java_index.get_columns.map(&:get_name)
47
+ end
48
+
49
+ # Checks whether this index is the table's primary key.
50
+ #
51
+ # @return [Boolean] true if this is a primary key index, false otherwise
52
+ #
53
+ # @example Check if primary key
54
+ # index.primary_key? # => true
55
+ def primary_key?
56
+ @java_index.is_primary_key
57
+ end
58
+
59
+ # Checks whether this index enforces uniqueness.
60
+ #
61
+ # @return [Boolean] true if values in this index must be unique, false otherwise
62
+ #
63
+ # @example Check if unique
64
+ # index.unique? # => true
65
+ def unique?
66
+ @java_index.is_unique
67
+ end
68
+
69
+ # Checks whether this index ignores null values.
70
+ #
71
+ # When true, null values in indexed columns are not included in the index.
72
+ #
73
+ # @return [Boolean] true if null values are ignored, false otherwise
74
+ #
75
+ # @example Check if nulls are ignored
76
+ # index.ignore_nulls? # => false
77
+ def ignore_nulls?
78
+ @java_index.should_ignore_nulls
79
+ end
80
+
81
+ def inspect
82
+ "#<Jackcess::Index name=#{name.inspect} columns=#{columns.inspect} primary_key=#{primary_key?}>"
83
+ end
84
+
85
+ def to_s
86
+ "#{name}: #{columns.join(', ')}"
87
+ end
88
+ end
89
+ end