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,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Duckdb
6
+ # DuckDB-specific schema dumper functionality for generating schema.rb files
7
+ # Provides methods to properly dump DuckDB-specific column types and constraints
8
+ module SchemaDumper
9
+ # Generates column specification for schema dumping
10
+ # @param column [ActiveRecord::ConnectionAdapters::Column] The column to generate spec for
11
+ # @return [Array] Array containing column type and options hash
12
+ def column_spec(column)
13
+ column_type = schema_type(column)
14
+ column_options = prepare_column_options(column)
15
+ # For any column with sequence defaults, include them in column options
16
+ if column.respond_to?(:default_function) && column.default_function&.include?('nextval(')
17
+ # Include the sequence function as column default
18
+ column_options[:default] = "-> { \"#{column.default_function}\" }"
19
+ end
20
+ [column_type, column_options]
21
+ end
22
+
23
+ # Maps DuckDB SQL types to ActiveRecord schema types
24
+ # @param column [ActiveRecord::ConnectionAdapters::Column] The column to map
25
+ # @return [Symbol] The ActiveRecord schema type symbol
26
+ def schema_type(column)
27
+ case column.sql_type.to_s.upcase
28
+ when /^BIGINT$/i
29
+ :bigint
30
+ when /^INTEGER$/i
31
+ :integer
32
+ when /^VARCHAR$/i, /^VARCHAR\(\d+\)$/i
33
+ :string
34
+ when /^TEXT$/i
35
+ :text
36
+ when /^TIMESTAMP$/i
37
+ :datetime
38
+ when /^BOOLEAN$/i
39
+ :boolean
40
+ when /^UUID$/i
41
+ :uuid
42
+ when /^DECIMAL\((\d+),(\d+)\)$/i
43
+ :decimal
44
+ when /^BLOB$/i
45
+ :binary
46
+ when /^REAL$/i, /^DOUBLE$/i
47
+ :float
48
+ when /^DATE$/i
49
+ :date
50
+ when /^TIME$/i
51
+ :time
52
+ else
53
+ column.type
54
+ end
55
+ end
56
+
57
+ # Determines if a column uses the default primary key behavior
58
+ # @param column [ActiveRecord::ConnectionAdapters::Column] The column to check
59
+ # @return [Boolean] true if column uses default primary key behavior
60
+ def default_primary_key?(column)
61
+ # Never treat sequence-based primary keys as having default behavior
62
+ return false if column.respond_to?(:default_function) && column.default_function&.include?('nextval(')
63
+
64
+ # Only consider it a default primary key if it's bigint without sequences
65
+ schema_type(column) == :bigint
66
+ end
67
+
68
+ # Determines if a primary key column requires explicit default inclusion
69
+ # @param column [ActiveRecord::ConnectionAdapters::Column] The column to check
70
+ # @return [Boolean] true if column requires explicit default in schema
71
+ def explicit_primary_key_default?(column)
72
+ # Return true for any column with sequence defaults to force explicit inclusion
73
+ column.respond_to?(:default_function) && column.default_function&.include?('nextval(')
74
+ end
75
+
76
+ # Prepares column options hash for schema dumping
77
+ # @param column [ActiveRecord::ConnectionAdapters::Column] The column to prepare options for
78
+ # @return [Hash] Hash of column options for schema dumping
79
+ def prepare_column_options(column)
80
+ spec = {}
81
+
82
+ # Add limit only for string types and when meaningful
83
+ if (limit = schema_limit(column))
84
+ spec[:limit] = limit
85
+ end
86
+
87
+ # Add precision only for numeric types and when meaningful (not nil/zero)
88
+ if (precision = schema_precision(column))
89
+ spec[:precision] = precision
90
+ end
91
+
92
+ # Add scale only for numeric types and when meaningful
93
+ if (scale = schema_scale(column))
94
+ spec[:scale] = scale
95
+ end
96
+
97
+ # Add null constraint
98
+ spec[:null] = false unless column.null
99
+
100
+ # Add default value if present and not a function (sequences handled in column_spec)
101
+ if (default = schema_default(column))
102
+ spec[:default] = default
103
+ end
104
+
105
+ # Add comment if present
106
+ spec[:comment] = column.comment.inspect if column.comment.present?
107
+ spec = spec.compact
108
+ spec
109
+ end
110
+
111
+ # Extracts limit option for schema dumping
112
+ # @param column [ActiveRecord::ConnectionAdapters::Column] The column to extract limit from
113
+ # @return [Integer, nil] The column limit or nil if not applicable
114
+ def schema_limit(column)
115
+ return column.limit if column.limit && column.type == :string
116
+
117
+ nil
118
+ end
119
+
120
+ # Extracts precision option for schema dumping
121
+ # @param column [ActiveRecord::ConnectionAdapters::Column] The column to extract precision from
122
+ # @return [Integer, nil] The column precision or nil if not applicable
123
+ def schema_precision(column)
124
+ return nil unless %i[decimal float numeric real].include?(column.type)
125
+ return nil unless column.precision&.positive?
126
+
127
+ column.precision
128
+ end
129
+
130
+ # Extracts scale option for schema dumping
131
+ # @param column [ActiveRecord::ConnectionAdapters::Column] The column to extract scale from
132
+ # @return [Integer, nil] The column scale or nil if not applicable
133
+ def schema_scale(column)
134
+ return nil unless %i[decimal float numeric real].include?(column.type)
135
+ return nil unless column.scale && column.scale >= 0
136
+
137
+ column.scale
138
+ end
139
+
140
+ # Extracts and formats default value for schema dumping
141
+ # @param column [ActiveRecord::ConnectionAdapters::Column] The column to extract default from
142
+ # @return [Object, nil] The formatted default value or nil if no default
143
+ def schema_default(column)
144
+ return nil if column.respond_to?(:default_function) && column.default_function
145
+
146
+ case column.default
147
+ when nil
148
+ nil
149
+ when true, false, Numeric
150
+ column.default
151
+ when String
152
+ # Handle DuckDB's boolean format: CAST('t' AS BOOLEAN) or CAST('f' AS BOOLEAN)
153
+ if column.default.match?(/^CAST\('([tf])' AS BOOLEAN\)$/i)
154
+ column.default.match(/^CAST\('([tf])' AS BOOLEAN\)$/i)[1].downcase == 't'
155
+ else
156
+ column.default.inspect
157
+ end
158
+ else
159
+ column.default.inspect
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end