| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package Concierge::Users::Database v0.8.0; |
|
2
|
6
|
|
|
6
|
|
970
|
use v5.36; |
|
|
6
|
|
|
|
|
22
|
|
|
3
|
6
|
|
|
6
|
|
31
|
use Carp qw/ croak /; |
|
|
6
|
|
|
|
|
21
|
|
|
|
6
|
|
|
|
|
493
|
|
|
4
|
6
|
|
|
6
|
|
9738
|
use DBI; |
|
|
6
|
|
|
|
|
131370
|
|
|
|
6
|
|
|
|
|
600
|
|
|
5
|
6
|
|
|
6
|
|
73
|
use parent qw/ Concierge::Users::Meta /; |
|
|
6
|
|
|
|
|
21
|
|
|
|
6
|
|
|
|
|
66
|
|
|
6
|
|
|
|
|
|
|
|
|
7
|
|
|
|
|
|
|
# ABSTRACT: Database backend for Concierge::Users |
|
8
|
|
|
|
|
|
|
|
|
9
|
|
|
|
|
|
|
# ============================================================================== |
|
10
|
|
|
|
|
|
|
# Configure Class Method - One-time setup (called by Users->setup) |
|
11
|
|
|
|
|
|
|
# ============================================================================== |
|
12
|
|
|
|
|
|
|
|
|
13
|
|
|
|
|
|
|
sub configure { |
|
14
|
29
|
|
|
29
|
1
|
117
|
my ($class, $setup_config) = @_; |
|
15
|
|
|
|
|
|
|
|
|
16
|
|
|
|
|
|
|
# Extract storage_dir |
|
17
|
29
|
|
|
|
|
73
|
my $storage_dir = $setup_config->{storage_dir}; |
|
18
|
|
|
|
|
|
|
|
|
19
|
|
|
|
|
|
|
# Build SQLite DSN and file path |
|
20
|
29
|
|
|
|
|
73
|
my $db_file = "$storage_dir/users.db"; |
|
21
|
29
|
|
|
|
|
78
|
my $dsn = "dbi:SQLite:$db_file"; |
|
22
|
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
# Connect to database |
|
24
|
29
|
|
|
|
|
447
|
my $dbh = DBI->connect($dsn, '', '', { |
|
25
|
|
|
|
|
|
|
RaiseError => 0, |
|
26
|
|
|
|
|
|
|
AutoCommit => 1, |
|
27
|
|
|
|
|
|
|
PrintError => 0, |
|
28
|
|
|
|
|
|
|
sqlite_unicode => 1, |
|
29
|
|
|
|
|
|
|
}); |
|
30
|
|
|
|
|
|
|
|
|
31
|
29
|
50
|
|
|
|
101326
|
unless ($dbh) { |
|
32
|
|
|
|
|
|
|
return { |
|
33
|
0
|
|
0
|
|
|
0
|
success => 0, |
|
34
|
|
|
|
|
|
|
message => sprintf( |
|
35
|
|
|
|
|
|
|
"Database backend connection failed:\n" . |
|
36
|
|
|
|
|
|
|
" - Database file: %s\n" . |
|
37
|
|
|
|
|
|
|
" - Error: %s", |
|
38
|
|
|
|
|
|
|
$db_file, |
|
39
|
|
|
|
|
|
|
$DBI::errstr || 'Unknown error' |
|
40
|
|
|
|
|
|
|
), |
|
41
|
|
|
|
|
|
|
}; |
|
42
|
|
|
|
|
|
|
} |
|
43
|
|
|
|
|
|
|
|
|
44
|
|
|
|
|
|
|
# Create temporary object for ensure_storage |
|
45
|
|
|
|
|
|
|
my $temp_backend = bless { |
|
46
|
|
|
|
|
|
|
dbh => $dbh, |
|
47
|
|
|
|
|
|
|
table_name => 'users', |
|
48
|
|
|
|
|
|
|
storage_dir => $storage_dir, |
|
49
|
|
|
|
|
|
|
db_file => $db_file, |
|
50
|
|
|
|
|
|
|
field_definitions => $setup_config->{field_definitions}, |
|
51
|
29
|
|
50
|
|
|
396
|
fields => $setup_config->{fields} || [], |
|
52
|
|
|
|
|
|
|
}, $class; |
|
53
|
|
|
|
|
|
|
|
|
54
|
|
|
|
|
|
|
# Check for existing data and archive if present |
|
55
|
29
|
|
|
|
|
71
|
my $check_sql = "SELECT name FROM sqlite_master WHERE type='table' AND name=?"; |
|
56
|
29
|
|
|
|
|
201
|
my $sth = $dbh->prepare($check_sql); |
|
57
|
29
|
50
|
|
|
|
2946
|
if ($sth) { |
|
58
|
29
|
|
|
|
|
2008
|
$sth->execute('users'); |
|
59
|
29
|
|
|
|
|
277
|
my ($table_exists) = $sth->fetchrow_array(); |
|
60
|
29
|
|
|
|
|
139
|
$sth->finish(); |
|
61
|
|
|
|
|
|
|
|
|
62
|
29
|
50
|
|
|
|
126
|
if ($table_exists) { |
|
63
|
|
|
|
|
|
|
# Check if table has data |
|
64
|
0
|
|
|
|
|
0
|
my $count_sql = "SELECT COUNT(*) FROM users"; |
|
65
|
0
|
|
|
|
|
0
|
my $count_sth = $dbh->prepare($count_sql); |
|
66
|
0
|
0
|
|
|
|
0
|
if ($count_sth) { |
|
67
|
0
|
|
|
|
|
0
|
$count_sth->execute(); |
|
68
|
0
|
|
|
|
|
0
|
my ($user_count) = $count_sth->fetchrow_array(); |
|
69
|
0
|
|
|
|
|
0
|
$count_sth->finish(); |
|
70
|
|
|
|
|
|
|
|
|
71
|
|
|
|
|
|
|
# Archive if table has data |
|
72
|
0
|
0
|
|
|
|
0
|
if ($user_count > 0) { |
|
73
|
0
|
|
|
|
|
0
|
my $archive_result = $temp_backend->_archive_user_data(); |
|
74
|
0
|
0
|
|
|
|
0
|
unless ($archive_result->{success}) { |
|
75
|
0
|
|
|
|
|
0
|
$temp_backend->disconnect(); |
|
76
|
|
|
|
|
|
|
return { |
|
77
|
|
|
|
|
|
|
success => 0, |
|
78
|
|
|
|
|
|
|
message => $archive_result->{message}, |
|
79
|
0
|
|
|
|
|
0
|
}; |
|
80
|
|
|
|
|
|
|
} |
|
81
|
|
|
|
|
|
|
} else { |
|
82
|
|
|
|
|
|
|
# Drop empty table |
|
83
|
0
|
|
|
|
|
0
|
$dbh->do("DROP TABLE users"); |
|
84
|
|
|
|
|
|
|
} |
|
85
|
|
|
|
|
|
|
} |
|
86
|
|
|
|
|
|
|
} |
|
87
|
|
|
|
|
|
|
} |
|
88
|
|
|
|
|
|
|
|
|
89
|
|
|
|
|
|
|
# Ensure storage (table) exists |
|
90
|
29
|
|
|
|
|
153
|
my $storage_ok = $temp_backend->ensure_storage(); |
|
91
|
29
|
50
|
|
|
|
215
|
unless ($storage_ok) { |
|
92
|
|
|
|
|
|
|
return { |
|
93
|
0
|
|
|
|
|
0
|
success => 0, |
|
94
|
|
|
|
|
|
|
message => "Failed to initialize storage for database backend", |
|
95
|
|
|
|
|
|
|
}; |
|
96
|
|
|
|
|
|
|
} |
|
97
|
|
|
|
|
|
|
|
|
98
|
|
|
|
|
|
|
# Disconnect temp object |
|
99
|
29
|
|
|
|
|
266
|
$temp_backend->disconnect(); |
|
100
|
|
|
|
|
|
|
|
|
101
|
|
|
|
|
|
|
# Return success with config |
|
102
|
|
|
|
|
|
|
return { |
|
103
|
|
|
|
|
|
|
success => 1, |
|
104
|
|
|
|
|
|
|
message => "Database backend configured successfully", |
|
105
|
|
|
|
|
|
|
config => { |
|
106
|
|
|
|
|
|
|
storage_dir => $storage_dir, |
|
107
|
|
|
|
|
|
|
db_file => 'users.db', |
|
108
|
|
|
|
|
|
|
db_full_path => $db_file, |
|
109
|
|
|
|
|
|
|
table_name => 'users', |
|
110
|
|
|
|
|
|
|
fields => $setup_config->{fields} || [], |
|
111
|
|
|
|
|
|
|
field_definitions => $setup_config->{field_definitions}, |
|
112
|
|
|
|
|
|
|
}, |
|
113
|
29
|
|
50
|
|
|
1713
|
}; |
|
114
|
|
|
|
|
|
|
} |
|
115
|
|
|
|
|
|
|
|
|
116
|
|
|
|
|
|
|
# ============================================================================== |
|
117
|
|
|
|
|
|
|
# Constructor - Runtime instantiation (called by Users->new) |
|
118
|
|
|
|
|
|
|
# ============================================================================== |
|
119
|
|
|
|
|
|
|
sub new { |
|
120
|
27
|
|
|
27
|
1
|
99
|
my ($class, $runtime_config) = @_; |
|
121
|
|
|
|
|
|
|
|
|
122
|
|
|
|
|
|
|
# Extract parameters from saved config (no validation needed) |
|
123
|
27
|
|
|
|
|
102
|
my $storage_dir = $runtime_config->{storage_dir}; |
|
124
|
27
|
|
|
|
|
105
|
my $db_file = $runtime_config->{db_full_path}; |
|
125
|
27
|
|
50
|
|
|
119
|
my $table_name = $runtime_config->{table_name} || 'users'; |
|
126
|
|
|
|
|
|
|
|
|
127
|
|
|
|
|
|
|
# Build SQLite DSN |
|
128
|
27
|
|
|
|
|
74
|
my $dsn = "dbi:SQLite:$db_file"; |
|
129
|
|
|
|
|
|
|
|
|
130
|
|
|
|
|
|
|
# Connect to database |
|
131
|
27
|
|
|
|
|
480
|
my $dbh = DBI->connect($dsn, '', '', { |
|
132
|
|
|
|
|
|
|
RaiseError => 0, |
|
133
|
|
|
|
|
|
|
AutoCommit => 1, |
|
134
|
|
|
|
|
|
|
PrintError => 0, |
|
135
|
|
|
|
|
|
|
sqlite_unicode => 1, |
|
136
|
|
|
|
|
|
|
}); |
|
137
|
|
|
|
|
|
|
|
|
138
|
27
|
50
|
|
|
|
24026
|
unless ($dbh) { |
|
139
|
0
|
|
0
|
|
|
0
|
croak sprintf( |
|
140
|
|
|
|
|
|
|
"Database backend connection failed:\n" . |
|
141
|
|
|
|
|
|
|
" - Database file: %s\n" . |
|
142
|
|
|
|
|
|
|
" - Error: %s", |
|
143
|
|
|
|
|
|
|
$db_file, |
|
144
|
|
|
|
|
|
|
$DBI::errstr || 'Unknown error' |
|
145
|
|
|
|
|
|
|
); |
|
146
|
|
|
|
|
|
|
} |
|
147
|
|
|
|
|
|
|
|
|
148
|
|
|
|
|
|
|
return bless { |
|
149
|
|
|
|
|
|
|
dbh => $dbh, |
|
150
|
|
|
|
|
|
|
table_name => $table_name, |
|
151
|
|
|
|
|
|
|
storage_dir => $storage_dir, |
|
152
|
|
|
|
|
|
|
db_file => $db_file, |
|
153
|
|
|
|
|
|
|
fields => $runtime_config->{fields} || [], |
|
154
|
|
|
|
|
|
|
field_definitions => $runtime_config->{field_definitions} || {}, |
|
155
|
27
|
|
50
|
|
|
438
|
}, $class; |
|
|
|
|
50
|
|
|
|
|
|
156
|
|
|
|
|
|
|
} |
|
157
|
|
|
|
|
|
|
|
|
158
|
|
|
|
|
|
|
# Report backend configuration (for debugging/info) |
|
159
|
|
|
|
|
|
|
sub config { |
|
160
|
0
|
|
|
0
|
0
|
0
|
my ($self) = @_; |
|
161
|
|
|
|
|
|
|
|
|
162
|
|
|
|
|
|
|
return { |
|
163
|
|
|
|
|
|
|
storage_dir => $self->{storage_dir}, |
|
164
|
|
|
|
|
|
|
db_file => $self->{db_file}, |
|
165
|
|
|
|
|
|
|
db_full_path => $self->{db_file}, |
|
166
|
|
|
|
|
|
|
table_name => $self->{table_name}, |
|
167
|
|
|
|
|
|
|
fields => $self->{fields}, |
|
168
|
|
|
|
|
|
|
field_definitions => $self->{field_definitions}, |
|
169
|
0
|
|
|
|
|
0
|
}; |
|
170
|
|
|
|
|
|
|
} |
|
171
|
|
|
|
|
|
|
|
|
172
|
|
|
|
|
|
|
# Ensure storage (table) exists |
|
173
|
|
|
|
|
|
|
sub ensure_storage { |
|
174
|
29
|
|
|
29
|
0
|
89
|
my ($self) = @_; |
|
175
|
|
|
|
|
|
|
|
|
176
|
|
|
|
|
|
|
# Check if table exists |
|
177
|
29
|
|
|
|
|
67
|
my $check_sql = "SELECT name FROM sqlite_master WHERE type='table' AND name=?"; |
|
178
|
29
|
|
|
|
|
221
|
my $sth = $self->{dbh}->prepare($check_sql); |
|
179
|
29
|
50
|
|
|
|
2198
|
return 0 unless $sth; |
|
180
|
29
|
|
|
|
|
1442
|
$sth->execute($self->{table_name}); |
|
181
|
29
|
|
|
|
|
185
|
my ($exists) = $sth->fetchrow_array(); |
|
182
|
|
|
|
|
|
|
|
|
183
|
29
|
50
|
|
|
|
143
|
return 1 if $exists; # Table already exists |
|
184
|
|
|
|
|
|
|
|
|
185
|
|
|
|
|
|
|
# Build CREATE TABLE SQL |
|
186
|
29
|
|
|
|
|
91
|
my @field_defs; |
|
187
|
|
|
|
|
|
|
my @indexes; |
|
188
|
|
|
|
|
|
|
|
|
189
|
29
|
|
|
|
|
55
|
foreach my $field (@{$self->{fields}}) { |
|
|
29
|
|
|
|
|
117
|
|
|
190
|
|
|
|
|
|
|
# All fields are TEXT in our schema |
|
191
|
340
|
|
|
|
|
627
|
my $field_def = "$field TEXT"; |
|
192
|
|
|
|
|
|
|
|
|
193
|
|
|
|
|
|
|
# Check if field is required |
|
194
|
340
|
|
|
|
|
657
|
my $field_def_info = $self->{field_definitions}{$field}; |
|
195
|
340
|
100
|
66
|
|
|
1161
|
if ($field_def_info && $field_def_info->{required}) { |
|
196
|
146
|
|
|
|
|
282
|
$field_def .= " NOT NULL"; |
|
197
|
|
|
|
|
|
|
} |
|
198
|
|
|
|
|
|
|
|
|
199
|
340
|
|
|
|
|
656
|
push @field_defs, $field_def; |
|
200
|
|
|
|
|
|
|
} |
|
201
|
|
|
|
|
|
|
|
|
202
|
|
|
|
|
|
|
# Primary key on user_id |
|
203
|
29
|
|
|
|
|
137
|
push @indexes, "CREATE UNIQUE INDEX idx_user_id ON $self->{table_name}(user_id)"; |
|
204
|
|
|
|
|
|
|
|
|
205
|
|
|
|
|
|
|
# Create table |
|
206
|
29
|
|
|
|
|
240
|
my $create_sql = "CREATE TABLE $self->{table_name} (\n" . |
|
207
|
|
|
|
|
|
|
join(",\n ", @field_defs) . "\n)"; |
|
208
|
|
|
|
|
|
|
|
|
209
|
29
|
|
|
|
|
272
|
my $result = $self->{dbh}->do($create_sql); |
|
210
|
29
|
50
|
|
|
|
519733
|
return 0 unless $result; |
|
211
|
|
|
|
|
|
|
|
|
212
|
|
|
|
|
|
|
# Create indexes |
|
213
|
29
|
|
|
|
|
150
|
foreach my $index_sql (@indexes) { |
|
214
|
29
|
|
|
|
|
648
|
$self->{dbh}->do($index_sql); |
|
215
|
|
|
|
|
|
|
} |
|
216
|
|
|
|
|
|
|
|
|
217
|
29
|
|
|
|
|
613081
|
return 1; |
|
218
|
|
|
|
|
|
|
} |
|
219
|
|
|
|
|
|
|
|
|
220
|
|
|
|
|
|
|
# Archive existing user data (internal method, called by configure) |
|
221
|
|
|
|
|
|
|
sub _archive_user_data { |
|
222
|
0
|
|
|
0
|
|
0
|
my ($self) = @_; |
|
223
|
|
|
|
|
|
|
|
|
224
|
|
|
|
|
|
|
# Generate timestamp for archive table name |
|
225
|
0
|
|
|
|
|
0
|
my $timestamp = $self->archive_timestamp(); |
|
226
|
0
|
|
|
|
|
0
|
my $archive_table = "$self->{table_name}_$timestamp"; |
|
227
|
|
|
|
|
|
|
|
|
228
|
|
|
|
|
|
|
# Rename table |
|
229
|
0
|
|
|
|
|
0
|
my $rename_sql = "ALTER TABLE $self->{table_name} RENAME TO $archive_table"; |
|
230
|
0
|
|
|
|
|
0
|
my $result = $self->{dbh}->do($rename_sql); |
|
231
|
|
|
|
|
|
|
|
|
232
|
0
|
0
|
|
|
|
0
|
unless ($result) { |
|
233
|
|
|
|
|
|
|
return { |
|
234
|
|
|
|
|
|
|
success => 0, |
|
235
|
|
|
|
|
|
|
message => "Failed to archive existing table: " . $self->{dbh}->errstr |
|
236
|
0
|
|
|
|
|
0
|
}; |
|
237
|
|
|
|
|
|
|
} |
|
238
|
|
|
|
|
|
|
|
|
239
|
0
|
|
|
|
|
0
|
return { success => 1 }; |
|
240
|
|
|
|
|
|
|
} |
|
241
|
|
|
|
|
|
|
|
|
242
|
|
|
|
|
|
|
# Add bare record with user_id and null_values |
|
243
|
|
|
|
|
|
|
sub add { |
|
244
|
80
|
|
|
80
|
1
|
366
|
my ($self, $user_id, $initial_record) = @_; |
|
245
|
80
|
50
|
|
|
|
325
|
return { success => 0, message => "Add Record failed: missing user_id" } |
|
246
|
|
|
|
|
|
|
unless $user_id; |
|
247
|
80
|
50
|
|
|
|
306
|
return { success => 0, message => "Add Record failed: missing initial record" } |
|
248
|
|
|
|
|
|
|
unless $initial_record; |
|
249
|
|
|
|
|
|
|
|
|
250
|
80
|
|
|
|
|
646
|
my %record = $initial_record->%*; |
|
251
|
80
|
|
|
|
|
670
|
$record{created_date} = $self->current_timestamp(); |
|
252
|
|
|
|
|
|
|
# Add last_mod_date timestamp |
|
253
|
80
|
|
|
|
|
280
|
$record{last_mod_date} = $self->current_timestamp(); |
|
254
|
|
|
|
|
|
|
|
|
255
|
|
|
|
|
|
|
# Insert into database |
|
256
|
80
|
|
|
|
|
333
|
my @fields = keys %record; |
|
257
|
80
|
|
|
|
|
300
|
my @placeholders = map { '?' } @fields; |
|
|
644
|
|
|
|
|
1186
|
|
|
258
|
80
|
|
|
|
|
483
|
my @values = @record{@fields}; |
|
259
|
|
|
|
|
|
|
|
|
260
|
80
|
|
|
|
|
1512
|
my $sql = "INSERT INTO $self->{table_name} (" . |
|
261
|
|
|
|
|
|
|
join(', ', @fields) . ") VALUES (" . |
|
262
|
|
|
|
|
|
|
join(', ', @placeholders) . ")"; |
|
263
|
|
|
|
|
|
|
|
|
264
|
80
|
|
|
|
|
640
|
my $sth = $self->{dbh}->prepare($sql); |
|
265
|
80
|
50
|
|
|
|
1691036
|
if ($sth->execute(@values)) { |
|
266
|
80
|
|
|
|
|
6000
|
return { success => 1, message => "Initial record created for user '$user_id'" }; |
|
267
|
|
|
|
|
|
|
} else { |
|
268
|
0
|
|
|
|
|
0
|
return { success => 0, message => "Failed to create initial user record: " . $self->{dbh}->errstr }; |
|
269
|
|
|
|
|
|
|
} |
|
270
|
|
|
|
|
|
|
} |
|
271
|
|
|
|
|
|
|
|
|
272
|
|
|
|
|
|
|
# Fetch user by ID |
|
273
|
|
|
|
|
|
|
sub fetch { |
|
274
|
108
|
|
|
108
|
1
|
488
|
my ($self, $user_id) = @_; |
|
275
|
|
|
|
|
|
|
|
|
276
|
108
|
|
|
|
|
441
|
my $sql = "SELECT * FROM $self->{table_name} WHERE user_id = ?"; |
|
277
|
108
|
|
|
|
|
1480
|
my $sth = $self->{dbh}->prepare($sql); |
|
278
|
108
|
|
|
|
|
26636
|
$sth->execute($user_id); |
|
279
|
|
|
|
|
|
|
|
|
280
|
108
|
|
|
|
|
4110
|
my $user_data = $sth->fetchrow_hashref(); |
|
281
|
|
|
|
|
|
|
|
|
282
|
|
|
|
|
|
|
return { |
|
283
|
108
|
100
|
|
|
|
3016
|
success => $user_data ? 1 : 0, |
|
|
|
100
|
|
|
|
|
|
|
284
|
|
|
|
|
|
|
data => $user_data, |
|
285
|
|
|
|
|
|
|
message => $user_data ? '' : "User '$user_id' not found" |
|
286
|
|
|
|
|
|
|
}; |
|
287
|
|
|
|
|
|
|
} |
|
288
|
|
|
|
|
|
|
|
|
289
|
|
|
|
|
|
|
# Update user |
|
290
|
|
|
|
|
|
|
sub update { |
|
291
|
84
|
|
|
84
|
1
|
355
|
my ($self, $user_id, $updates) = @_; |
|
292
|
|
|
|
|
|
|
|
|
293
|
|
|
|
|
|
|
# Remove readonly fields from updates |
|
294
|
84
|
|
|
|
|
335
|
my %readonly = map { $_ => 1 } qw(user_id created_date last_mod_date); |
|
|
252
|
|
|
|
|
884
|
|
|
295
|
84
|
|
|
|
|
659
|
delete $updates->{$_} for keys %readonly; |
|
296
|
|
|
|
|
|
|
|
|
297
|
|
|
|
|
|
|
# Add last_mod_date timestamp |
|
298
|
84
|
|
|
|
|
502
|
$updates->{last_mod_date} = $self->current_timestamp(); |
|
299
|
|
|
|
|
|
|
|
|
300
|
84
|
|
|
|
|
346
|
my @fields = keys %$updates; |
|
301
|
84
|
|
|
|
|
280
|
my @values = values %$updates; |
|
302
|
84
|
|
|
|
|
194
|
push @values, $user_id; # For WHERE clause |
|
303
|
|
|
|
|
|
|
|
|
304
|
|
|
|
|
|
|
my $sql = "UPDATE $self->{table_name} SET " . |
|
305
|
84
|
|
|
|
|
389
|
join(', ', map { "$_ = ?" } @fields) . |
|
|
165
|
|
|
|
|
755
|
|
|
306
|
|
|
|
|
|
|
" WHERE user_id = ?"; |
|
307
|
|
|
|
|
|
|
|
|
308
|
84
|
|
|
|
|
1299
|
my $sth = $self->{dbh}->prepare($sql); |
|
309
|
84
|
50
|
|
|
|
13536
|
unless ($sth) { |
|
310
|
0
|
|
|
|
|
0
|
return { success => 0, message => "Failed to prepare update: " . $self->{dbh}->errstr }; |
|
311
|
|
|
|
|
|
|
} |
|
312
|
|
|
|
|
|
|
|
|
313
|
84
|
50
|
|
|
|
1464657
|
if ($sth->execute(@values)) { |
|
314
|
84
|
|
|
|
|
5336
|
return { success => 1, message => "User '$user_id' updated" }; |
|
315
|
|
|
|
|
|
|
} else { |
|
316
|
0
|
|
|
|
|
0
|
return { success => 0, message => "Failed to update user: " . $self->{dbh}->errstr }; |
|
317
|
|
|
|
|
|
|
} |
|
318
|
|
|
|
|
|
|
} |
|
319
|
|
|
|
|
|
|
|
|
320
|
|
|
|
|
|
|
# List users with filters |
|
321
|
|
|
|
|
|
|
sub list { |
|
322
|
7
|
|
|
7
|
1
|
24
|
my ($self, $filters, $options) = @_; |
|
323
|
|
|
|
|
|
|
|
|
324
|
|
|
|
|
|
|
# Build WHERE clause from DSL filter structure |
|
325
|
7
|
|
|
|
|
18
|
my @where_clauses; |
|
326
|
|
|
|
|
|
|
my @where_values; |
|
327
|
|
|
|
|
|
|
|
|
328
|
7
|
100
|
66
|
|
|
84
|
if (ref $filters eq 'HASH' && exists $filters->{or_groups}) { |
|
329
|
|
|
|
|
|
|
# Parse DSL filter structure |
|
330
|
2
|
|
|
|
|
4
|
my @or_groups; |
|
331
|
|
|
|
|
|
|
|
|
332
|
2
|
|
|
|
|
4
|
foreach my $and_group (@{$filters->{or_groups}}) { |
|
|
2
|
|
|
|
|
7
|
|
|
333
|
2
|
|
|
|
|
3
|
my @and_clauses; |
|
334
|
2
|
|
|
|
|
5
|
foreach my $condition (@$and_group) { |
|
335
|
2
|
|
|
|
|
8
|
my ($field, $op, $value) = ($condition->{field}, $condition->{op}, $condition->{value}); |
|
336
|
|
|
|
|
|
|
|
|
337
|
2
|
|
|
|
|
25
|
my $clause; |
|
338
|
2
|
100
|
|
|
|
10
|
if ($op eq '=') { |
|
|
|
50
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
339
|
1
|
|
|
|
|
4
|
$clause = "$field = ?"; |
|
340
|
1
|
|
|
|
|
3
|
push @where_values, $value; |
|
341
|
|
|
|
|
|
|
} elsif ($op eq ':') { |
|
342
|
1
|
|
|
|
|
3
|
$clause = "$field LIKE ?"; |
|
343
|
1
|
|
|
|
|
4
|
push @where_values, "%$value%"; |
|
344
|
|
|
|
|
|
|
} elsif ($op eq '!') { |
|
345
|
0
|
|
|
|
|
0
|
$clause = "$field NOT LIKE ?"; |
|
346
|
0
|
|
|
|
|
0
|
push @where_values, "%$value%"; |
|
347
|
|
|
|
|
|
|
} elsif ($op eq '>') { |
|
348
|
0
|
|
|
|
|
0
|
$clause = "$field > ?"; |
|
349
|
0
|
|
|
|
|
0
|
push @where_values, $value; |
|
350
|
|
|
|
|
|
|
} elsif ($op eq '<') { |
|
351
|
0
|
|
|
|
|
0
|
$clause = "$field < ?"; |
|
352
|
0
|
|
|
|
|
0
|
push @where_values, $value; |
|
353
|
|
|
|
|
|
|
} else { |
|
354
|
0
|
|
|
|
|
0
|
next; # Skip unknown operators |
|
355
|
|
|
|
|
|
|
} |
|
356
|
|
|
|
|
|
|
|
|
357
|
2
|
|
|
|
|
6
|
push @and_clauses, $clause; |
|
358
|
|
|
|
|
|
|
} |
|
359
|
|
|
|
|
|
|
|
|
360
|
|
|
|
|
|
|
# Join AND conditions |
|
361
|
2
|
50
|
|
|
|
6
|
if (@and_clauses) { |
|
362
|
2
|
|
|
|
|
10
|
push @or_groups, "(" . join(" AND ", @and_clauses) . ")"; |
|
363
|
|
|
|
|
|
|
} |
|
364
|
|
|
|
|
|
|
} |
|
365
|
|
|
|
|
|
|
|
|
366
|
|
|
|
|
|
|
# Join OR groups |
|
367
|
2
|
50
|
|
|
|
5
|
if (@or_groups) { |
|
368
|
2
|
|
|
|
|
7
|
push @where_clauses, join(" OR ", @or_groups); |
|
369
|
|
|
|
|
|
|
} |
|
370
|
|
|
|
|
|
|
} |
|
371
|
|
|
|
|
|
|
|
|
372
|
|
|
|
|
|
|
# Data query |
|
373
|
7
|
|
|
|
|
45
|
my $sql = "SELECT * FROM $self->{table_name}"; |
|
374
|
7
|
100
|
|
|
|
28
|
if (@where_clauses) { |
|
375
|
2
|
|
|
|
|
6
|
$sql .= " WHERE " . join(' AND ', @where_clauses); |
|
376
|
|
|
|
|
|
|
} |
|
377
|
|
|
|
|
|
|
|
|
378
|
7
|
|
|
|
|
84
|
my $sth = $self->{dbh}->prepare($sql); |
|
379
|
7
|
|
|
|
|
1381
|
$sth->execute(@where_values); |
|
380
|
|
|
|
|
|
|
|
|
381
|
7
|
|
|
|
|
26
|
my @users; |
|
382
|
7
|
|
|
|
|
288
|
while (my $row = $sth->fetchrow_hashref()) { |
|
383
|
62
|
|
|
|
|
1126
|
push @users, $row; |
|
384
|
|
|
|
|
|
|
} |
|
385
|
|
|
|
|
|
|
|
|
386
|
|
|
|
|
|
|
return { |
|
387
|
7
|
|
|
|
|
159
|
data => \@users, |
|
388
|
|
|
|
|
|
|
total_count => scalar @users, |
|
389
|
|
|
|
|
|
|
}; |
|
390
|
|
|
|
|
|
|
} |
|
391
|
|
|
|
|
|
|
|
|
392
|
|
|
|
|
|
|
# Delete user |
|
393
|
|
|
|
|
|
|
sub delete { |
|
394
|
1
|
|
|
1
|
1
|
4
|
my ($self, $user_id) = @_; |
|
395
|
|
|
|
|
|
|
|
|
396
|
1
|
|
|
|
|
3
|
my $sql = "DELETE FROM $self->{table_name} WHERE user_id = ?"; |
|
397
|
1
|
|
|
|
|
5
|
my $sth = $self->{dbh}->prepare($sql); |
|
398
|
|
|
|
|
|
|
|
|
399
|
1
|
50
|
|
|
|
23215
|
if ($sth->execute($user_id)) { |
|
400
|
1
|
|
|
|
|
47
|
return { success => 1, message => "User '$user_id' deleted" }; |
|
401
|
|
|
|
|
|
|
} else { |
|
402
|
0
|
|
|
|
|
0
|
return { success => 0, message => "Failed to delete user: " . $self->{dbh}->errstr }; |
|
403
|
|
|
|
|
|
|
} |
|
404
|
|
|
|
|
|
|
} |
|
405
|
|
|
|
|
|
|
|
|
406
|
|
|
|
|
|
|
# Cleanup |
|
407
|
|
|
|
|
|
|
sub disconnect { |
|
408
|
112
|
|
|
112
|
1
|
276
|
my $self = shift; |
|
409
|
112
|
100
|
|
|
|
5691
|
if ($self->{dbh}) { |
|
410
|
56
|
|
|
|
|
6656
|
$self->{dbh}->disconnect(); |
|
411
|
56
|
|
|
|
|
2538
|
$self->{dbh} = undef; |
|
412
|
|
|
|
|
|
|
} |
|
413
|
|
|
|
|
|
|
} |
|
414
|
|
|
|
|
|
|
|
|
415
|
|
|
|
|
|
|
sub DESTROY { |
|
416
|
56
|
|
|
56
|
|
230
|
my $self = shift; |
|
417
|
56
|
|
|
|
|
221
|
$self->disconnect(); |
|
418
|
|
|
|
|
|
|
} |
|
419
|
|
|
|
|
|
|
|
|
420
|
|
|
|
|
|
|
1; |
|
421
|
|
|
|
|
|
|
|
|
422
|
|
|
|
|
|
|
__END__ |