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,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jackcess
4
+ class Row
5
+ attr_reader :java_row, :table
6
+ attr_accessor :data
7
+
8
+ def initialize(java_row, table)
9
+ @java_row = java_row # This is actually a Map from Jackcess
10
+ @table = table
11
+ @data = {}
12
+ @original_data = {}
13
+ @dirty = false
14
+
15
+ # Convert Java map to Ruby hash
16
+ if java_row
17
+ java_row.each do |key, value|
18
+ converted_value = TypeConverter.from_java(value)
19
+ @data[key.to_s] = converted_value
20
+ @original_data[key.to_s] = converted_value
21
+ end
22
+ end
23
+ end
24
+
25
+ # Get column value by name
26
+ def [](column_name)
27
+ raise ArgumentError, "Column name cannot be nil" if column_name.nil?
28
+
29
+ @data[column_name.to_s]
30
+ end
31
+
32
+ # Set column value by name
33
+ def []=(column_name, value)
34
+ raise ArgumentError, "Column name cannot be nil" if column_name.nil?
35
+
36
+ column_name = column_name.to_s
37
+
38
+ # Mark as dirty if value changed
39
+ if @data[column_name] != value
40
+ @dirty = true
41
+ end
42
+
43
+ @data[column_name] = value
44
+ end
45
+
46
+ # Save changes to the database
47
+ def save
48
+ return nil unless @dirty
49
+
50
+ begin
51
+ # Get primary key to find the row
52
+ pk_index = @table.java_table.get_primary_key_index
53
+ raise DatabaseError, "Cannot update row: table has no primary key" if pk_index.nil?
54
+
55
+ pk_column_name = pk_index.get_columns.first.get_name
56
+ pk_value = @data[pk_column_name]
57
+
58
+ # Create a new cursor for this update operation
59
+ cursor = com.healthmarketscience.jackcess.CursorBuilder.create_cursor(@table.java_table)
60
+
61
+ while cursor.move_to_next_row
62
+ current_row = cursor.get_current_row
63
+ current_pk = TypeConverter.from_java(current_row.get(pk_column_name))
64
+
65
+ if current_pk == pk_value
66
+ # Update each changed column value
67
+ @data.each do |column_name, value|
68
+ # Skip if value hasn't changed
69
+ next if @original_data[column_name] == value
70
+
71
+ column = @table.java_table.get_column(column_name)
72
+ cursor.setCurrentRowValue(column, TypeConverter.to_java(value))
73
+ end
74
+
75
+ # Update original data to match current data
76
+ @original_data = @data.dup
77
+ @dirty = false
78
+
79
+ return true
80
+ end
81
+ end
82
+
83
+ raise DatabaseError, "Row not found for update"
84
+ rescue Java::JavaIo::IOException => e
85
+ raise DatabaseError, "Failed to save row: #{e.message}"
86
+ rescue Java::JavaLang::Exception => e
87
+ raise DatabaseError, "Failed to save row: #{e.message}"
88
+ end
89
+ end
90
+
91
+ # Delete this row
92
+ def delete
93
+ begin
94
+ # Get primary key to find the row
95
+ pk_index = @table.java_table.get_primary_key_index
96
+ raise DatabaseError, "Cannot delete row: table has no primary key" if pk_index.nil?
97
+
98
+ pk_column_name = pk_index.get_columns.first.get_name
99
+ pk_value = @data[pk_column_name]
100
+
101
+ # Create a new cursor for this delete operation
102
+ cursor = com.healthmarketscience.jackcess.CursorBuilder.create_cursor(@table.java_table)
103
+
104
+ while cursor.move_to_next_row
105
+ current_row = cursor.get_current_row
106
+ current_pk = TypeConverter.from_java(current_row.get(pk_column_name))
107
+
108
+ if current_pk == pk_value
109
+ cursor.deleteCurrentRow
110
+ return true
111
+ end
112
+ end
113
+
114
+ raise DatabaseError, "Row not found for deletion"
115
+ rescue Java::JavaIo::IOException => e
116
+ raise DatabaseError, "Failed to delete row: #{e.message}"
117
+ rescue Java::JavaLang::Exception => e
118
+ raise DatabaseError, "Failed to delete row: #{e.message}"
119
+ end
120
+ end
121
+
122
+ # Get all column names
123
+ def keys
124
+ @data.keys
125
+ end
126
+
127
+ # Get all values
128
+ def values
129
+ @data.values
130
+ end
131
+
132
+ # Convert to hash
133
+ def to_h
134
+ @data.dup
135
+ end
136
+
137
+ # Check if row has been modified
138
+ def dirty?
139
+ @dirty
140
+ end
141
+
142
+ # Reload row from database
143
+ def reload
144
+ begin
145
+ # Get primary key to find the row
146
+ pk_index = @table.java_table.get_primary_key_index
147
+ raise DatabaseError, "Cannot reload row: table has no primary key" if pk_index.nil?
148
+
149
+ pk_column_name = pk_index.get_columns.first.get_name
150
+ pk_value = @original_data[pk_column_name]
151
+
152
+ # Find the row again
153
+ reloaded_row = @table.find_by_id(pk_value)
154
+
155
+ if reloaded_row
156
+ @data = reloaded_row.data.dup
157
+ @original_data = reloaded_row.data.dup
158
+ @dirty = false
159
+ end
160
+
161
+ self
162
+ rescue Java::JavaIo::IOException => e
163
+ raise DatabaseError, "Failed to reload row: #{e.message}"
164
+ rescue Java::JavaLang::Exception => e
165
+ raise DatabaseError, "Failed to reload row: #{e.message}"
166
+ end
167
+ end
168
+
169
+ def inspect
170
+ "#<Jackcess::Row #{@data.inspect}>"
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jackcess
4
+ class Table
5
+ include Enumerable
6
+
7
+ attr_reader :java_table, :database
8
+
9
+ def initialize(java_table, database)
10
+ @java_table = java_table
11
+ @database = database
12
+ end
13
+
14
+ # Get table name
15
+ def name
16
+ @java_table.get_name
17
+ end
18
+
19
+ # Iterate through all rows
20
+ def each
21
+ return enum_for(:each) unless block_given?
22
+
23
+ begin
24
+ # Create a new cursor for iteration to avoid state issues
25
+ cursor = com.healthmarketscience.jackcess.CursorBuilder.create_cursor(@java_table)
26
+ while cursor.move_to_next_row
27
+ java_row = cursor.get_current_row
28
+ yield Row.new(java_row, self)
29
+ end
30
+ rescue Java::JavaIo::IOException => e
31
+ raise DatabaseError, "Failed to iterate rows: #{e.message}"
32
+ end
33
+ end
34
+
35
+ # Add a new row
36
+ def add_row(data)
37
+ raise ArgumentError, "Data cannot be nil" if data.nil?
38
+ raise ArgumentError, "Data must be a Hash" unless data.is_a?(Hash)
39
+
40
+ begin
41
+ java_row_data = java.util.HashMap.new
42
+ data.each do |key, value|
43
+ java_row_data.put(key.to_s, TypeConverter.to_java(value))
44
+ end
45
+
46
+ java_row = @java_table.add_row_from_map(java_row_data)
47
+ Row.new(java_row, self)
48
+ rescue Java::JavaIo::IOException => e
49
+ raise DatabaseError, "Failed to add row to table '#{name}': #{e.message}"
50
+ rescue Java::JavaLang::Exception => e
51
+ raise DatabaseError, "Failed to add row to table '#{name}': #{e.message}"
52
+ end
53
+ end
54
+
55
+ # Find row by primary key
56
+ def find_by_id(id)
57
+ begin
58
+ primary_key_index = @java_table.get_primary_key_index
59
+
60
+ if primary_key_index.nil?
61
+ # No primary key, do a linear search
62
+ each do |row|
63
+ return row if row[primary_key_columns.first] == id
64
+ end
65
+ nil
66
+ else
67
+ # Use index search
68
+ search_map = java.util.HashMap.new
69
+ search_map.put(primary_key_index.get_columns.first.get_name, TypeConverter.to_java(id))
70
+
71
+ java_row = com.healthmarketscience.jackcess.CursorBuilder.find_row(@java_table, search_map)
72
+ java_row ? Row.new(java_row, self) : nil
73
+ end
74
+ rescue Java::JavaIo::IOException => e
75
+ raise DatabaseError, "Failed to find row: #{e.message}"
76
+ end
77
+ end
78
+
79
+ # Get all columns
80
+ def columns
81
+ @java_table.get_columns.map { |java_col| Column.new(java_col) }
82
+ end
83
+
84
+ # Get column by name
85
+ def column(name)
86
+ raise ArgumentError, "Column name cannot be nil" if name.nil?
87
+ raise ArgumentError, "Column name must be a String or Symbol" unless name.is_a?(String) || name.is_a?(Symbol)
88
+
89
+ name = name.to_s
90
+ begin
91
+ java_col = @java_table.get_column(name)
92
+ raise ColumnNotFoundError, "Column not found: #{name}" if java_col.nil?
93
+
94
+ Column.new(java_col)
95
+ rescue Java::JavaLang::IllegalArgumentException => e
96
+ raise ColumnNotFoundError, "Column not found: #{name}"
97
+ end
98
+ end
99
+
100
+ # Get all indexes
101
+ def indexes
102
+ @java_table.get_indexes.map { |java_idx| Index.new(java_idx) }
103
+ end
104
+
105
+ # Get row count
106
+ def row_count
107
+ @java_table.get_row_count
108
+ end
109
+ alias size row_count
110
+ alias length row_count
111
+
112
+ # Find rows matching the given criteria.
113
+ #
114
+ # This method provides a simple filtering interface for finding rows.
115
+ # For more complex queries, iterate through rows with {#each} and filter manually.
116
+ #
117
+ # @param criteria [Hash] Column name/value pairs to match
118
+ #
119
+ # @return [Array<Row>] Array of matching rows
120
+ #
121
+ # @example Find users by name
122
+ # users = table.where('Name' => 'Alice')
123
+ #
124
+ # @example Find with multiple criteria
125
+ # users = table.where('Name' => 'Bob', 'Active' => true)
126
+ def where(criteria)
127
+ raise ArgumentError, "Criteria cannot be nil" if criteria.nil?
128
+ raise ArgumentError, "Criteria must be a Hash" unless criteria.is_a?(Hash)
129
+
130
+ select { |row| criteria.all? { |key, value| row[key.to_s] == value } }
131
+ end
132
+
133
+ # Find the first row matching the given criteria.
134
+ #
135
+ # @param criteria [Hash] Column name/value pairs to match
136
+ #
137
+ # @return [Row, nil] The first matching row, or nil if no match
138
+ #
139
+ # @example Find first user by name
140
+ # user = table.find_first('Name' => 'Alice')
141
+ def find_first(criteria)
142
+ raise ArgumentError, "Criteria cannot be nil" if criteria.nil?
143
+ raise ArgumentError, "Criteria must be a Hash" unless criteria.is_a?(Hash)
144
+
145
+ detect { |row| criteria.all? { |key, value| row[key.to_s] == value } }
146
+ end
147
+
148
+ # Count rows matching the given criteria.
149
+ #
150
+ # @param criteria [Hash, nil] Column name/value pairs to match (nil counts all rows)
151
+ #
152
+ # @return [Integer] The number of matching rows
153
+ #
154
+ # @example Count all rows
155
+ # table.count # => 100
156
+ #
157
+ # @example Count filtered rows
158
+ # table.count('Active' => true) # => 75
159
+ def count(criteria = nil)
160
+ return row_count if criteria.nil?
161
+
162
+ raise ArgumentError, "Criteria must be a Hash" unless criteria.is_a?(Hash)
163
+
164
+ where(criteria).size
165
+ end
166
+
167
+ # Check if any rows match the given criteria.
168
+ #
169
+ # @param criteria [Hash] Column name/value pairs to match
170
+ #
171
+ # @return [Boolean] true if at least one row matches, false otherwise
172
+ #
173
+ # @example Check if any active users exist
174
+ # table.any?('Active' => true) # => true
175
+ def any?(criteria)
176
+ raise ArgumentError, "Criteria cannot be nil" if criteria.nil?
177
+ raise ArgumentError, "Criteria must be a Hash" unless criteria.is_a?(Hash)
178
+
179
+ !find_first(criteria).nil?
180
+ end
181
+
182
+ private
183
+
184
+ def primary_key_columns
185
+ pk_index = @java_table.get_primary_key_index
186
+ return [] if pk_index.nil?
187
+
188
+ pk_index.get_columns.map(&:get_name)
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+ require "date"
5
+
6
+ module Jackcess
7
+ module TypeConverter
8
+ class << self
9
+ # Convert Java types to Ruby types
10
+ def from_java(value)
11
+ return nil if value.nil?
12
+
13
+ case value
14
+ when Java::JavaLang::String
15
+ value.to_s
16
+ when Java::JavaLang::Integer, Java::JavaLang::Short, Java::JavaLang::Byte
17
+ value.to_i
18
+ when Java::JavaLang::Long
19
+ value.to_i
20
+ when Java::JavaLang::Float, Java::JavaLang::Double
21
+ value.to_f
22
+ when Java::JavaLang::Boolean
23
+ value.boolean_value
24
+ when Java::JavaUtil::Date
25
+ Time.at(value.time / 1000.0)
26
+ when Java::JavaMath::BigDecimal
27
+ BigDecimal(value.to_s)
28
+ when Java::byte[]
29
+ String.from_java_bytes(value)
30
+ else
31
+ value
32
+ end
33
+ end
34
+
35
+ # Convert Ruby types to Java types
36
+ def to_java(value)
37
+ return nil if value.nil?
38
+
39
+ case value
40
+ when String
41
+ value.to_java
42
+ when Integer
43
+ # Use appropriate Java type based on value range
44
+ if value >= -2_147_483_648 && value <= 2_147_483_647
45
+ java.lang.Integer.new(value)
46
+ else
47
+ java.lang.Long.new(value)
48
+ end
49
+ when Float
50
+ java.lang.Double.new(value)
51
+ when TrueClass, FalseClass
52
+ java.lang.Boolean.new(value)
53
+ when Time
54
+ java.util.Date.new((value.to_f * 1000).to_i)
55
+ when Date
56
+ # Convert Ruby Date to Time first, then to Java Date
57
+ java.util.Date.new((value.to_time.to_f * 1000).to_i)
58
+ when BigDecimal
59
+ java.math.BigDecimal.new(value.to_s)
60
+ else
61
+ value
62
+ end
63
+ end
64
+
65
+ # Map Ruby type symbols to Jackcess DataType enums
66
+ def ruby_type_to_data_type(type_sym)
67
+ case type_sym
68
+ when :text
69
+ DataType::TEXT
70
+ when :memo
71
+ DataType::MEMO
72
+ when :byte
73
+ DataType::BYTE
74
+ when :int
75
+ DataType::INT
76
+ when :long
77
+ DataType::LONG
78
+ when :float
79
+ DataType::FLOAT
80
+ when :double
81
+ DataType::DOUBLE
82
+ when :currency
83
+ DataType::MONEY
84
+ when :date_time
85
+ DataType::SHORT_DATE_TIME
86
+ when :boolean
87
+ DataType::BOOLEAN
88
+ when :binary
89
+ DataType::BINARY
90
+ when :guid
91
+ DataType::GUID
92
+ else
93
+ raise InvalidTypeError, "Unknown type: #{type_sym}"
94
+ end
95
+ end
96
+
97
+ # Map Jackcess DataType to Ruby type symbol
98
+ def data_type_to_ruby_type(data_type)
99
+ case data_type.to_s
100
+ when "TEXT"
101
+ :text
102
+ when "MEMO"
103
+ :memo
104
+ when "BYTE"
105
+ :byte
106
+ when "INT"
107
+ :int
108
+ when "LONG"
109
+ :long
110
+ when "FLOAT"
111
+ :float
112
+ when "DOUBLE"
113
+ :double
114
+ when "MONEY"
115
+ :currency
116
+ when "SHORT_DATE_TIME"
117
+ :date_time
118
+ when "BOOLEAN"
119
+ :boolean
120
+ when "BINARY", "OLE"
121
+ :binary
122
+ when "GUID"
123
+ :guid
124
+ else
125
+ :unknown
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jackcess
4
+ # The current version of the jackcess-rb gem.
5
+ #
6
+ # This follows Semantic Versioning 2.0.0 (https://semver.org/):
7
+ # - MAJOR version: Incompatible API changes
8
+ # - MINOR version: Backwards-compatible functionality additions
9
+ # - PATCH version: Backwards-compatible bug fixes
10
+ VERSION = "0.1.0"
11
+ end
data/lib/jackcess.rb ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ unless RUBY_PLATFORM == "java"
4
+ raise LoadError, "jackcess-rb requires JRuby. You are running #{RUBY_DESCRIPTION}"
5
+ end
6
+
7
+ require "java"
8
+
9
+ # Load the Jackcess JAR and its dependencies
10
+ vendor_dir = File.expand_path("../vendor", __dir__)
11
+ require File.join(vendor_dir, "commons-logging-1.2.jar")
12
+ require File.join(vendor_dir, "commons-lang3-3.13.0.jar")
13
+ require File.join(vendor_dir, "jackcess-4.0.4.jar")
14
+
15
+ # Import necessary Java classes
16
+ java_import "com.healthmarketscience.jackcess.DatabaseBuilder"
17
+ java_import "com.healthmarketscience.jackcess.Database"
18
+ java_import "com.healthmarketscience.jackcess.Table"
19
+ java_import "com.healthmarketscience.jackcess.Column"
20
+ java_import "com.healthmarketscience.jackcess.Row"
21
+ java_import "com.healthmarketscience.jackcess.DataType"
22
+ java_import "com.healthmarketscience.jackcess.ColumnBuilder"
23
+ java_import "com.healthmarketscience.jackcess.TableBuilder"
24
+
25
+ require_relative "jackcess/version"
26
+ require_relative "jackcess/type_converter"
27
+ require_relative "jackcess/database"
28
+ require_relative "jackcess/table"
29
+ require_relative "jackcess/row"
30
+ require_relative "jackcess/column"
31
+ require_relative "jackcess/index"
32
+ require_relative "jackcess/errors"
33
+
34
+ module Jackcess
35
+ end
Binary file
Binary file
Binary file
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jackcess-rb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: java
6
+ authors:
7
+ - Durable Programming LLC
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-01 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: jackcess-rb provides a Ruby-friendly interface to the powerful Jackcess
13
+ Java library, enabling programmatic interaction with Access database files (.mdb
14
+ and .accdb) directly from JRuby.
15
+ email:
16
+ - commercial@durableprogramming.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - CHANGELOG.md
22
+ - LICENSE
23
+ - README.md
24
+ - lib/jackcess.rb
25
+ - lib/jackcess/column.rb
26
+ - lib/jackcess/database.rb
27
+ - lib/jackcess/errors.rb
28
+ - lib/jackcess/index.rb
29
+ - lib/jackcess/row.rb
30
+ - lib/jackcess/table.rb
31
+ - lib/jackcess/type_converter.rb
32
+ - lib/jackcess/version.rb
33
+ - vendor/commons-lang3-3.13.0.jar
34
+ - vendor/commons-logging-1.2.jar
35
+ - vendor/jackcess-4.0.4.jar
36
+ homepage: https://github.com/durableprogramming/jackcess-rb
37
+ licenses:
38
+ - MIT
39
+ metadata:
40
+ homepage_uri: https://github.com/durableprogramming/jackcess-rb
41
+ source_code_uri: https://github.com/durableprogramming/jackcess-rb
42
+ changelog_uri: https://github.com/durableprogramming/jackcess-rb/blob/main/CHANGELOG.md
43
+ rdoc_options: []
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 2.6.0
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ requirements:
57
+ - Java 8 or higher
58
+ - JRuby 9.3 or higher
59
+ rubygems_version: 3.6.9
60
+ specification_version: 4
61
+ summary: JRuby interface to the Jackcess library for reading and writing Microsoft
62
+ Access databases
63
+ test_files: []