activerecord-duckdb 0.1.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,322 @@
1
+ # DuckDB TableDefinition Guide
2
+
3
+ This guide covers the `TableDefinition` class for the ActiveRecord DuckDB adapter, which provides full Rails compatibility plus support for DuckDB-specific data types.
4
+
5
+ ## Overview
6
+
7
+ The DuckDB adapter's `TableDefinition` follows standard Rails conventions while adding:
8
+
9
+ - **Rails Compatibility**: All standard Rails table creation patterns work seamlessly
10
+ - **Flexible Primary Keys**: Support for integer, bigint, UUID, and string primary keys using Rails conventions
11
+ - **DuckDB Data Types**: Support for DuckDB-specific types like `HUGEINT`, `UUID`, `LIST`, `STRUCT`, etc.
12
+ - **Automatic Sequences**: Behind-the-scenes sequence management for integer primary keys
13
+ </text>
14
+
15
+ <old_text>
16
+
17
+ ### Integer Primary Keys (with Sequences)
18
+
19
+ ```ruby
20
+ # Standard Rails table - automatically creates bigint id with sequence
21
+ create_table :users do |t|
22
+ t.string :name
23
+ t.timestamps
24
+ end
25
+ # DuckDB adapter automatically creates users_id_seq sequence
26
+
27
+ # Explicit integer primary key (Rails 4 style)
28
+ create_table :posts, id: :integer do |t|
29
+ t.string :title
30
+ t.timestamps
31
+ end
32
+ # Creates posts_id_seq sequence with integer type
33
+
34
+ # Explicit bigint primary key (Rails 5+ default)
35
+ create_table :articles, id: :bigint do |t|
36
+ t.string :title
37
+ t.timestamps
38
+ end
39
+ # Creates articles_id_seq sequence
40
+ ```
41
+
42
+ ## Primary Key Support
43
+
44
+ ### Integer Primary Keys (with Sequences)
45
+
46
+ ```ruby
47
+ # Standard Rails table - automatically creates bigint id with sequence
48
+ create_table :users do |t|
49
+ t.string :name
50
+ t.timestamps
51
+ end
52
+ # DuckDB adapter automatically creates users_id_seq sequence
53
+
54
+ # Explicit integer primary key (Rails 4 style)
55
+ create_table :posts, id: :integer do |t|
56
+ t.string :title
57
+ t.timestamps
58
+ end
59
+ # Creates posts_id_seq sequence with integer type
60
+
61
+ # Explicit bigint primary key (Rails 5+ default)
62
+ create_table :articles, id: :bigint do |t|
63
+ t.string :title
64
+ t.timestamps
65
+ end
66
+ # Creates articles_id_seq sequence
67
+ ```
68
+
69
+ ### UUID Primary Keys (no sequences needed)
70
+
71
+ ```ruby
72
+ # Rails convention for UUID primary keys
73
+ create_table :sessions, id: :uuid do |t|
74
+ t.string :session_token
75
+ t.timestamps
76
+ end
77
+ # No sequence created - UUIDs are generated by the application
78
+
79
+ # UUID with custom primary key name
80
+ create_table :articles, primary_key: :article_uuid, id: :uuid do |t|
81
+ t.string :title
82
+ t.timestamps
83
+ end
84
+ ```
85
+
86
+ ### String Primary Keys
87
+
88
+ ```ruby
89
+ # Rails convention for string primary keys
90
+ create_table :categories, id: :string do |t|
91
+ t.string :name
92
+ end
93
+ # No sequence created - string IDs managed by application
94
+
95
+ # Custom string primary key name
96
+ create_table :countries, primary_key: :iso_code, id: :string do |t|
97
+ t.string :name
98
+ end
99
+ ```
100
+
101
+ ## Sequence Management
102
+
103
+ ### Advanced Sequence Management
104
+
105
+ If you need custom sequence behavior, you can use the sequence methods directly:
106
+
107
+ ```ruby
108
+ # Create a custom sequence
109
+ connection.create_sequence('custom_seq', start_with: 1000, increment_by: 10)
110
+
111
+ # Use in a table (manual approach)
112
+ create_table :invoices do |t|
113
+ t.bigint :invoice_number, default: -> { "nextval('custom_seq')" }
114
+ t.decimal :amount
115
+ t.timestamps
116
+ end
117
+ ```
118
+
119
+ ## DuckDB-Specific Column Types
120
+
121
+ ### Integer Variants
122
+
123
+ ```ruby
124
+ create_table :analytics do |t|
125
+ t.tinyint :small_counter # 1 byte: -128 to 127
126
+ t.smallint :medium_counter # 2 bytes: -32,768 to 32,767
127
+ t.integer :standard_counter # 4 bytes (standard)
128
+ t.bigint :large_counter # 8 bytes
129
+ t.hugeint :massive_counter # 16 bytes: very large integers
130
+
131
+ # Unsigned variants
132
+ t.utinyint :positive_small # 1 byte: 0 to 255
133
+ t.usmallint :positive_medium # 2 bytes: 0 to 65,535
134
+ t.uinteger :positive_standard # 4 bytes: 0 to 4,294,967,295
135
+ t.ubigint :positive_large # 8 bytes
136
+ t.uhugeint :positive_massive # 16 bytes
137
+
138
+ t.varint :flexible_precision # Variable precision (up to 1.2M digits)
139
+ end
140
+ ```
141
+
142
+ ### Special Data Types
143
+
144
+ ```ruby
145
+ create_table :advanced_types do |t|
146
+ t.uuid :identifier # UUID type
147
+ t.interval :duration # Time intervals
148
+ t.bit :flags # Bit strings
149
+ end
150
+ ```
151
+
152
+ ### Complex/Nested Types
153
+
154
+ ```ruby
155
+ create_table :complex_data do |t|
156
+ # List/Array columns
157
+ t.list :tags, element_type: :string
158
+ t.list :scores, element_type: :integer
159
+
160
+ # Struct columns (composite types)
161
+ t.struct :address, fields: {
162
+ street: :string,
163
+ city: :string,
164
+ zip: :integer,
165
+ country: :string
166
+ }
167
+
168
+ # Map columns (key-value pairs)
169
+ t.map :metadata, key_type: :string, value_type: :string
170
+ t.map :counters, key_type: :string, value_type: :integer
171
+
172
+ # Enum columns
173
+ t.enum :status, values: ['active', 'inactive', 'pending', 'archived']
174
+ t.enum :priority, values: ['low', 'medium', 'high', 'critical']
175
+ end
176
+ ```
177
+
178
+ ## Migration Examples
179
+
180
+ ### Basic Table with Integer Primary Key
181
+
182
+ ```ruby
183
+ class CreateUsers < ActiveRecord::Migration[7.0]
184
+ def change
185
+ # Standard Rails migration - works exactly like other databases
186
+ create_table :users do |t|
187
+ t.string :email, null: false
188
+ t.string :name
189
+ t.timestamps
190
+ end
191
+ end
192
+ end
193
+ ```
194
+
195
+ ### Table with UUID Primary Key
196
+
197
+ ```ruby
198
+ class CreateSessions < ActiveRecord::Migration[7.0]
199
+ def change
200
+ # Rails convention for UUID primary keys
201
+ create_table :sessions, id: :uuid do |t|
202
+ t.string :session_token
203
+ t.references :user, type: :bigint, foreign_key: true
204
+ t.timestamps
205
+ end
206
+ end
207
+ end
208
+ ```
209
+
210
+ ### Complex Data Types Example
211
+
212
+ ```ruby
213
+ class CreateProducts < ActiveRecord::Migration[7.0]
214
+ def change
215
+ # Standard Rails table with DuckDB-specific types
216
+ create_table :products do |t|
217
+ t.string :name, null: false
218
+ t.decimal :price, precision: 10, scale: 2
219
+
220
+ # DuckDB complex data types
221
+ t.list :tags, element_type: :string
222
+ t.struct :dimensions, fields: {
223
+ length: :decimal,
224
+ width: :decimal,
225
+ height: :decimal,
226
+ unit: :string
227
+ }
228
+ t.map :attributes, key_type: :string, value_type: :string
229
+ t.enum :status, values: ['draft', 'active', 'discontinued']
230
+
231
+ t.timestamps
232
+ end
233
+ end
234
+ end
235
+ ```
236
+
237
+ ### Working with Sequences in Existing Tables
238
+
239
+ ```ruby
240
+ class AddAutoIncrementToItems < ActiveRecord::Migration[7.0]
241
+ def change
242
+ # Create sequence and add column manually if needed
243
+ create_sequence 'items_serial_number_seq', start_with: 1
244
+
245
+ change_table :items do |t|
246
+ t.bigint :serial_number, default: -> { "nextval('items_serial_number_seq')" }
247
+ end
248
+ end
249
+ end
250
+ ```
251
+
252
+ ## Adapter Configuration
253
+
254
+ ### Setting Default Primary Key Type
255
+
256
+ ```ruby
257
+ # In your application configuration or initializer
258
+ # The adapter defaults to :bigint (Rails 5+ convention)
259
+ ActiveRecord::ConnectionAdapters::DuckdbAdapter.primary_key_type = :bigint
260
+
261
+ # To change default to UUID globally
262
+ ActiveRecord::ConnectionAdapters::DuckdbAdapter.primary_key_type = :uuid
263
+
264
+ # Individual tables can still override with id: :integer, id: :uuid, etc.
265
+ ```
266
+
267
+ ## Sequence Operations
268
+
269
+ ### Working with Sequences in Code
270
+
271
+ ```ruby
272
+ # Get next value from a sequence
273
+ next_id = User.connection.next_sequence_value('users_id_seq')
274
+
275
+ # Reset sequence to specific value
276
+ User.connection.reset_sequence!('users_id_seq', 1000)
277
+
278
+ # Check if sequence exists
279
+ if User.connection.sequence_exists?('users_id_seq')
280
+ # Do something with the sequence
281
+ end
282
+
283
+ # List all sequences
284
+ sequences = User.connection.sequences
285
+ ```
286
+
287
+ ## Best Practices
288
+
289
+ 1. **Follow Rails conventions**: Use standard `create_table` with `id: :uuid` or `id: :bigint` options
290
+ 2. **Use appropriate integer types**: Choose the smallest integer type that fits your needs
291
+ 3. **Consider UUID for distributed systems**: Use `id: :uuid` for distributed applications
292
+ 4. **Leverage complex types**: Use DuckDB's advanced types like `LIST` and `STRUCT` for analytical workloads
293
+ 5. **Trust Rails defaults**: The adapter handles sequences automatically for integer primary keys
294
+ 6. **Use manual sequences sparingly**: Only create custom sequences when you need specific behavior
295
+
296
+ ## Troubleshooting
297
+
298
+ ### Common Issues
299
+
300
+ 1. **Sequence already exists**: If you get sequence already exists errors, check if the sequence was created from a previous migration
301
+ 2. **UUID dependencies**: Make sure your application can generate UUIDs when using UUID primary keys
302
+ 3. **Complex type support**: Verify your DuckDB version supports the complex types you're trying to use
303
+
304
+ ### Debugging Sequences
305
+
306
+ ```ruby
307
+ # Check if a sequence exists
308
+ connection.sequence_exists?('users_id_seq')
309
+ # => true
310
+
311
+ # List all sequences
312
+ connection.sequences
313
+ # => ['users_id_seq', 'products_id_seq', ...]
314
+
315
+ # Standard Rails way - sequences are handled automatically
316
+ create_table :test_table do |t|
317
+ t.string :name
318
+ end
319
+ # Sequence creation is managed behind the scenes
320
+ ```
321
+
322
+ This `TableDefinition` provides seamless Rails compatibility plus access to DuckDB's advanced features, making it easy to build both traditional Rails applications and analytical workloads with DuckDB.
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Duckdb
6
+ class Column < ConnectionAdapters::Column
7
+ # Initialize a new DuckDB column with DuckDB-specific attributes
8
+ # @param auto_increment [Boolean, nil] whether this column is auto-incrementing
9
+ # @param rowid [Boolean, nil] whether this column is a rowid column
10
+ # @param generated_type [Symbol, nil] the type of generated column (:stored, :virtual)
11
+ # @param extra [String, nil] extra column definition information
12
+ def initialize(*, auto_increment: nil, rowid: nil, generated_type: nil, extra: nil, **)
13
+ super(*, **)
14
+ @auto_increment = auto_increment
15
+ @rowid = rowid
16
+ @generated_type = generated_type
17
+ @extra = extra
18
+ end
19
+
20
+ # Quotes a column name for use in SQL statements
21
+ # @param name [String, Symbol] The column name to quote
22
+ # @return [String] The quoted column name wrapped in double quotes
23
+ def quote_column_name(name)
24
+ %("#{name}")
25
+ end
26
+
27
+ # @return [String, nil] extra column definition information
28
+ attr_reader :extra
29
+ # @return [Boolean, nil] whether this column is a rowid column
30
+ attr_reader :rowid
31
+
32
+ # Check if this column is a virtual/generated column
33
+ # https://duckdb.org/docs/stable/sql/statements/create_table#generated-columns
34
+ # TODO: Implement full virtual column support
35
+ # @return [Boolean] always returns false until virtual columns are fully implemented
36
+ def virtual?
37
+ # /\b(?:VIRTUAL|STORED|GENERATED)\b/.match?(extra)
38
+ false
39
+ end
40
+
41
+ # def virtual_stored?
42
+ # virtual? && @generated_type == :stored
43
+ # end
44
+
45
+ # Check if this column has a default value
46
+ # @return [Boolean] true if the column has a default value
47
+ def has_default?
48
+ # super && !virtual?
49
+ !!super
50
+ end
51
+
52
+ # Check if this column is auto-incrementing
53
+ # We probably need to check if the column is a UUID
54
+ # @return [Boolean] true if the column auto-increments
55
+ def auto_increment?
56
+ !!@auto_increment
57
+ end
58
+
59
+ # Check if this column's value is automatically incremented by the database
60
+ # @return [Boolean] true if the column is auto-incremented by the database
61
+ def auto_incremented_by_db?
62
+ auto_increment? || !!rowid
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ # DuckDB Limits - https://duckdb.org/docs/stable/operations_manual/limits.html
6
+ module Duckdb
7
+ module DatabaseLimits
8
+ # Since DuckDb is PostgreSQL compatible (64-1), we can use the same limits as PostgreSQL.
9
+ # In the future want to drop possible PostgreSQL compatibility
10
+ # we can change the limits to match whatever limits DuckDB may want to impose
11
+ # https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
12
+ # Returns the maximum length for database identifiers
13
+ # @return [Integer] The maximum identifier length (63 characters for PostgreSQL compatibility)
14
+ def max_identifier_length
15
+ 63
16
+ end
17
+
18
+ # Returns the maximum length for table aliases
19
+ # @return [Integer] The maximum table alias length (delegates to max_identifier_length)
20
+ def table_alias_length
21
+ max_identifier_length
22
+ end
23
+
24
+ # Returns the maximum length for table names
25
+ # @return [Integer] The maximum table name length (delegates to max_identifier_length)
26
+ def table_name_length
27
+ max_identifier_length
28
+ end
29
+
30
+ # Returns the maximum length for index names
31
+ # @return [Integer] The maximum index name length (delegates to max_identifier_length)
32
+ def index_name_length
33
+ max_identifier_length
34
+ end
35
+
36
+ private
37
+
38
+ # The max number of binds is 1024
39
+ # Returns the maximum number of bind parameters for queries
40
+ # @return [Integer] The maximum number of bind parameters (1000)
41
+ def bind_params_length
42
+ 1_000
43
+ end
44
+
45
+ # https://duckdb.org/docs/stable/data/insert.html
46
+ # Returns the maximum number of rows that can be inserted in a single operation
47
+ # @return [Integer] The maximum number of rows for batch inserts (1000)
48
+ def insert_rows_length
49
+ 1_000
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end